From bd8333dac03bb29802ae8651267adddf9e4d1fd1 Mon Sep 17 00:00:00 2001 From: DevPanther Date: Thu, 21 Sep 2023 18:08:41 +0100 Subject: [PATCH 01/23] chore: create table generator function --- src/helpers/comment.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/helpers/comment.ts b/src/helpers/comment.ts index cba6c165f..123b2a87b 100644 --- a/src/helpers/comment.ts +++ b/src/helpers/comment.ts @@ -40,3 +40,40 @@ export const parseComments = (comments: string[], itemsToExclude: string[]): Rec return result; }; + +export const createDetailsTable = (amount: string, paymentURL: string, values: { header: string; label: string; value: string }[]): string => { + // Generate the table rows based on the values array + const tableRows = values + .map(({ label, value, header }) => { + return ` + + ${header || ""} + ${label} + ${value} + + `; + }) + .join(""); + + // Construct the complete HTML structure + const html = ` +
+ + +

+ [ ${amount} ] +

+
+
+ + + + ${tableRows} + +
+
+
+ `; + + return html; +}; From ead1bceadf8049bfd62cd538563ec0587b7eb45f Mon Sep 17 00:00:00 2001 From: DevPanther Date: Thu, 21 Sep 2023 18:10:57 +0100 Subject: [PATCH 02/23] chore: add username to table generator --- src/helpers/comment.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/helpers/comment.ts b/src/helpers/comment.ts index 123b2a87b..0b2108ed6 100644 --- a/src/helpers/comment.ts +++ b/src/helpers/comment.ts @@ -41,7 +41,12 @@ export const parseComments = (comments: string[], itemsToExclude: string[]): Rec return result; }; -export const createDetailsTable = (amount: string, paymentURL: string, values: { header: string; label: string; value: string }[]): string => { +export const createDetailsTable = ( + amount: string, + paymentURL: string, + username: string, + values: { header: string; label: string; value: string }[] +): string => { // Generate the table rows based on the values array const tableRows = values .map(({ label, value, header }) => { @@ -63,6 +68,7 @@ export const createDetailsTable = (amount: string, paymentURL: string, values: {

[ ${amount} ]

+
 @${username}
From 3c475f15662478da4a8001c6f17a1062979d9acd Mon Sep 17 00:00:00 2001 From: DevPanther Date: Thu, 21 Sep 2023 21:06:35 +0100 Subject: [PATCH 03/23] chore: setting up comment creation --- package.json | 2 +- src/handlers/payout/action.ts | 149 +++++++++++++++++++++------------- src/handlers/payout/post.ts | 6 +- src/helpers/comment.ts | 22 +++-- 4 files changed, 109 insertions(+), 70 deletions(-) diff --git a/package.json b/package.json index a7c0704c8..3ce7f9102 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "lint": "eslint --ext .ts ./src", "start:serverless": "tsx src/adapters/github/github-actions.ts", "start:watch": "nodemon --exec 'yarn start'", - "start": "probot run ./lib/src/index.js", + "start": "probot run ./lib/index.js", "prepare": "husky install" }, "dependencies": { diff --git a/src/handlers/payout/action.ts b/src/handlers/payout/action.ts index 0afe9c424..b65540640 100644 --- a/src/handlers/payout/action.ts +++ b/src/handlers/payout/action.ts @@ -9,13 +9,12 @@ import { generatePermit2Signature, getAllIssueComments, getTokenSymbol, - savePermitToDB, wasIssueReopened, getAllIssueAssignEvents, addCommentToIssue, + createDetailsTable, } from "../../helpers"; import { UserType, Payload, StateReason, Comment, User, Incentives, Issue } from "../../types"; -import { shortenEthAddress } from "../../utils"; import { bountyInfo } from "../wildcard"; import Decimal from "decimal.js"; import { GLOBAL_STRINGS } from "../../configs"; @@ -53,8 +52,9 @@ export interface RewardByUser { priceInEth: Decimal; userId: string | undefined; issueId: string; - type: string | undefined; + type: (string | undefined)[]; user: string | undefined; + priceArray: string[]; } /** @@ -293,6 +293,7 @@ export const calculateIssueAssigneeReward = async (incentivesCalculation: Incent const account = await getWalletAddress(assigneeLogin); return { + title: "Issue-Assignee", error: "", userId: incentivesCalculation.assignee.node_id, username: assigneeLogin, @@ -320,7 +321,7 @@ export const handleIssueClosed = async ( const issueNumber = incentivesCalculation.issue.number; let commentersComment = "", - title = "Task Assignee", + title = ["Issue-Assignee"], assigneeComment = "", creatorComment = "", mergedComment = "", @@ -343,8 +344,6 @@ export const handleIssueClosed = async ( // COMMENTERS REWARD HANDLER if (conversationRewards.reward && conversationRewards.reward.length > 0) { - commentersComment = `#### ${conversationRewards.title} Rewards \n`; - conversationRewards.reward.map(async (permit) => { // Exclude issue creator from commenter rewards if (permit.userId !== creatorReward.userId) { @@ -353,8 +352,9 @@ export const handleIssueClosed = async ( priceInEth: permit.priceInEth, userId: permit.userId, issueId: incentivesCalculation.issue.node_id, - type: conversationRewards.title, + type: [conversationRewards.title], user: permit.user, + priceArray: [permit.priceInEth.toString()], }); } }); @@ -362,8 +362,6 @@ export const handleIssueClosed = async ( // PULL REQUEST REVIEWERS REWARD HANDLER if (pullRequestReviewersReward.reward && pullRequestReviewersReward.reward.length > 0) { - pullRequestReviewerComment = `#### ${pullRequestReviewersReward.title} Rewards \n`; - pullRequestReviewersReward.reward.map(async (permit) => { // Exclude issue creator from commenter rewards if (permit.userId !== creatorReward.userId) { @@ -372,8 +370,9 @@ export const handleIssueClosed = async ( priceInEth: permit.priceInEth, userId: permit.userId, issueId: incentivesCalculation.issue.node_id, - type: pullRequestReviewersReward.title, + type: [pullRequestReviewersReward.title], user: permit.user, + priceArray: [permit.priceInEth.toString()], }); } }); @@ -382,19 +381,29 @@ export const handleIssueClosed = async ( // CREATOR REWARD HANDLER // Generate permit for user if its not the same id as assignee if (creatorReward && creatorReward.reward && creatorReward.reward[0].account !== "0x" && creatorReward.userId !== incentivesCalculation.assignee.node_id) { - const { payoutUrl } = await generatePermit2Signature( - creatorReward.reward[0].account, - creatorReward.reward[0].priceInEth, - incentivesCalculation.issue.node_id, - creatorReward.userId - ); - - creatorComment = `#### ${creatorReward.title} Reward \n### [ **${creatorReward.username}: [ CLAIM ${ - creatorReward.reward[0].priceInEth - } ${incentivesCalculation.tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n`; - if (payoutUrl) { - logger.info(`Permit url generated for creator. reward: ${payoutUrl}`); - } + // const { payoutUrl } = await generatePermit2Signature( + // creatorReward.reward[0].account, + // creatorReward.reward[0].priceInEth, + // incentivesCalculation.issue.node_id, + // creatorReward.userId + // ); + + rewardByUser.push({ + account: creatorReward.reward[0].account, + priceInEth: creatorReward.reward[0].priceInEth, + userId: creatorReward.userId, + issueId: incentivesCalculation.issue.node_id, + type: [creatorReward.title], + user: creatorReward.username, + priceArray: [creatorReward.reward[0].priceInEth.toString()], + }); + + // creatorComment = `#### ${creatorReward.title} Reward \n### [ **${creatorReward.username}: [ CLAIM ${ + // creatorReward.reward[0].priceInEth + // } ${incentivesCalculation.tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n`; + // if (payoutUrl) { + // logger.info(`Permit url generated for creator. reward: ${payoutUrl}`); + // } // Add amount to assignee if assignee is the creator } else if ( creatorReward && @@ -403,30 +412,40 @@ export const handleIssueClosed = async ( creatorReward.userId === incentivesCalculation.assignee.node_id ) { priceInEth = priceInEth.add(creatorReward.reward[0].priceInEth); - title += " and Creator"; + title.push("Issue-Creation"); } else if (creatorReward && creatorReward.reward && creatorReward.reward[0].account === "0x") { logger.info(`Skipping to generate a permit url for missing account. fallback: ${creatorReward.fallbackReward}`); } // ASSIGNEE REWARD HANDLER if (assigneeReward && assigneeReward.reward && assigneeReward.reward[0].account !== "0x") { - const { txData, payoutUrl } = await generatePermit2Signature( - assigneeReward.reward[0].account, - assigneeReward.reward[0].priceInEth, - incentivesCalculation.issue.node_id, - incentivesCalculation.assignee.node_id - ); - const tokenSymbol = await getTokenSymbol(incentivesCalculation.paymentToken, incentivesCalculation.rpc); - const shortenRecipient = shortenEthAddress(assigneeReward.reward[0].account, `[ CLAIM ${priceInEth} ${tokenSymbol.toUpperCase()} ]`.length); - logger.info(`Posting a payout url to the issue, url: ${payoutUrl}`); - assigneeComment = - `#### ${title} Reward \n### [ **[ CLAIM ${priceInEth} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n` + "```" + shortenRecipient + "```"; + // const { txData, payoutUrl } = await generatePermit2Signature( + // assigneeReward.reward[0].account, + // assigneeReward.reward[0].priceInEth, + // incentivesCalculation.issue.node_id, + // incentivesCalculation.assignee.node_id + // ); + // const tokenSymbol = await getTokenSymbol(incentivesCalculation.paymentToken, incentivesCalculation.rpc); + // const shortenRecipient = shortenEthAddress(assigneeReward.reward[0].account, `[ CLAIM ${priceInEth} ${tokenSymbol.toUpperCase()} ]`.length); + // logger.info(`Posting a payout url to the issue, url: ${payoutUrl}`); + // assigneeComment = + // `#### ${title} Reward \n### [ **[ CLAIM ${priceInEth} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n` + "```" + shortenRecipient + "```"; const permitComments = incentivesCalculation.comments.filter((content) => { const permitUrlMatches = content.body.match(incentivesCalculation.claimUrlRegex); if (!permitUrlMatches || permitUrlMatches.length < 2) return false; else return true; }); + rewardByUser.push({ + account: assigneeReward.reward[0].account, + priceInEth: assigneeReward.reward[0].priceInEth, + userId: assigneeReward.userId, + issueId: incentivesCalculation.issue.node_id, + type: title, + user: assigneeReward.username, + priceArray: [assigneeReward.reward[0].priceInEth.toString()], + }); + if (permitComments.length > 0) { logger.info(`Skip to generate a permit url because it has been already posted.`); return { error: `Permit generation disabled because it was already posted to this issue.` }; @@ -441,8 +460,6 @@ export const handleIssueClosed = async ( assigneeReward.reward[0].penaltyAmount ); } - - await savePermitToDB(incentivesCalculation.assignee.id, txData); } // MERGE ALL REWARDS @@ -450,14 +467,16 @@ export const handleIssueClosed = async ( const existing = acc.find((item) => item.userId === curr.userId); if (existing) { existing.priceInEth = existing.priceInEth.add(curr.priceInEth); - // merge type by adding comma and - existing.type = `${existing.type} and ${curr.type}`; + existing.priceArray = existing.priceArray.concat(curr.priceArray); + existing.type = existing.type.concat(curr.type); } else { acc.push(curr); } return acc; }, [] as RewardByUser[]); + const tokenSymbol = await getTokenSymbol(incentivesCalculation.paymentToken, incentivesCalculation.rpc); + // CREATE PERMIT URL FOR EACH USER for (const reward of rewards) { const { payoutUrl } = await generatePermit2Signature(reward.account, reward.priceInEth, reward.issueId, reward.userId); @@ -467,25 +486,39 @@ export const handleIssueClosed = async ( continue; } - switch (reward.type) { - case "Conversation and Reviewer": - case "Reviewer and Conversation": - if (mergedComment === "") mergedComment = `#### ${reward.type} Rewards `; - mergedComment = `${mergedComment}\n### [ **${reward.user}: [ CLAIM ${ - reward.priceInEth - } ${incentivesCalculation.tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n`; + const detailsValue = reward.priceArray.map((price, i) => { + const separateTitle = reward.type[i]?.split("-"); + if (!separateTitle) return { title: "", subtitle: "", value: "" }; + return { title: separateTitle[0], subtitle: separateTitle[1], value: price }; + }); + + const mergedType = reward.type.join(","); + const price = `${reward.priceInEth} ${tokenSymbol.toUpperCase()}`; + + switch (mergedType) { + case "Issue-Comments,Review-Reviewer": + const responseOne = createDetailsTable(price, payoutUrl, reward.user, detailsValue); + console.log(responseOne); break; - case "Conversation": - commentersComment = `${commentersComment}\n### [ **${reward.user}: [ CLAIM ${ - reward.priceInEth - } ${incentivesCalculation.tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n`; - commentersReward[reward.user] = payoutUrl; + case "Issue-Comments": + const responseTwo = createDetailsTable(price, payoutUrl, reward.user, detailsValue); + console.log(responseTwo); break; - case "Reviewer": - pullRequestReviewerComment = `${pullRequestReviewerComment}\n### [ **${reward.user}: [ CLAIM ${ - reward.priceInEth - } ${incentivesCalculation.tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n`; - prReviewersReward[reward.user] = payoutUrl; + case "Review-Reviewer": + const responseThree = createDetailsTable(price, payoutUrl, reward.user, detailsValue); + console.log(responseThree); + break; + case "Issue-Assignee,Issue-Creation": + const responseFour = createDetailsTable(price, payoutUrl, reward.user, detailsValue); + console.log(responseFour); + break; + case "Issue-Assignee": + const responseFive = createDetailsTable(price, payoutUrl, reward.user, detailsValue); + console.log(responseFive); + break; + case "Issue-Creation": + const responseSix = createDetailsTable(price, payoutUrl, reward.user, detailsValue); + console.log(responseSix); break; default: break; @@ -498,6 +531,8 @@ export const handleIssueClosed = async ( logger.info(`Skipping to generate a permit url for missing accounts. fallback: ${JSON.stringify(pullRequestReviewersReward.fallbackReward)}`); } + //await savePermitToDB(incentivesCalculation.assignee.id, txData); + if (commentersComment && !isEmpty(commentersReward)) await addCommentToIssue(commentersComment, issueNumber); if (creatorComment) await addCommentToIssue(creatorComment, issueNumber); if (pullRequestReviewerComment && !isEmpty(prReviewersReward)) await addCommentToIssue(pullRequestReviewerComment, issueNumber); @@ -505,7 +540,7 @@ export const handleIssueClosed = async ( if (assigneeComment) await addCommentToIssue(assigneeComment + comments.promotionComment, issueNumber); await deleteLabel(incentivesCalculation.issueDetailed.priceLabel); - await addLabelToIssue("Permitted"); + //await addLabelToIssue("Permitted"); return { error: "" }; }; diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index 18d9aa1df..48d2793ac 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -25,7 +25,7 @@ const ItemsToExclude: string[] = [MarkdownItem.BlockQuote]; * The default formula has been defined in https://github.com/ubiquity/ubiquibot/issues/272 */ export const calculateIssueConversationReward = async (calculateIncentives: IncentivesCalculationResult): Promise => { - const title = `Conversation`; + const title = `Issue-Comments`; const logger = getLogger(); const context = getBotContext(); @@ -103,7 +103,7 @@ export const calculateIssueConversationReward = async (calculateIncentives: Ince }; export const calculateIssueCreatorReward = async (incentivesCalculation: IncentivesCalculationResult): Promise => { - const title = `Task Creator`; + const title = `Issue-Creation`; const logger = getLogger(); const issueDetailed = bountyInfo(incentivesCalculation.issue); @@ -172,7 +172,7 @@ export const calculateIssueCreatorReward = async (incentivesCalculation: Incenti export const calculatePullRequestReviewsReward = async (incentivesCalculation: IncentivesCalculationResult): Promise => { const logger = getLogger(); const context = getBotContext(); - const title = "Reviewer"; + const title = "Review-Reviewer"; const linkedPullRequest = await gitLinkedPrParser({ owner: incentivesCalculation.payload.repository.owner.login, diff --git a/src/helpers/comment.ts b/src/helpers/comment.ts index 0b2108ed6..3ac8aa909 100644 --- a/src/helpers/comment.ts +++ b/src/helpers/comment.ts @@ -45,18 +45,19 @@ export const createDetailsTable = ( amount: string, paymentURL: string, username: string, - values: { header: string; label: string; value: string }[] + values: { title: string; subtitle: string; value: string }[] ): string => { // Generate the table rows based on the values array const tableRows = values - .map(({ label, value, header }) => { - return ` - - ${header || ""} - ${label} + .map(({ title, value, subtitle }) => { + if (!subtitle || !value) { + return ""; + } + return ` + ${title || ""} + ${subtitle} ${value} - - `; + `; }) .join(""); @@ -81,5 +82,8 @@ export const createDetailsTable = ( `; - return html; + // Remove spaces and line breaks from the HTML, ignoring the attributes like and [ ... ] + const cleanedHtml = html.replace(/>\s+<").replace(/[\r\n]+/g, ""); + + return cleanedHtml; }; From c1478520b29eb11f8da4568dd03cb5bf213db257 Mon Sep 17 00:00:00 2001 From: DevPanther Date: Thu, 21 Sep 2023 21:35:51 +0100 Subject: [PATCH 04/23] chore: concatenate comment and clear unused --- src/handlers/payout/action.ts | 99 +++++++++++------------------------ src/helpers/comment.ts | 12 ++--- 2 files changed, 35 insertions(+), 76 deletions(-) diff --git a/src/handlers/payout/action.ts b/src/handlers/payout/action.ts index b65540640..92da597dc 100644 --- a/src/handlers/payout/action.ts +++ b/src/handlers/payout/action.ts @@ -13,6 +13,7 @@ import { getAllIssueAssignEvents, addCommentToIssue, createDetailsTable, + savePermitToDB, } from "../../helpers"; import { UserType, Payload, StateReason, Comment, User, Incentives, Issue } from "../../types"; import { bountyInfo } from "../wildcard"; @@ -20,7 +21,6 @@ import Decimal from "decimal.js"; import { GLOBAL_STRINGS } from "../../configs"; import { isParentIssue } from "../pricing"; import { RewardsResponse } from "../comment"; -import { isEmpty } from "lodash"; export interface IncentivesCalculationResult { paymentToken: string; @@ -320,12 +320,9 @@ export const handleIssueClosed = async ( const { comments } = getBotConfig(); const issueNumber = incentivesCalculation.issue.number; - let commentersComment = "", - title = ["Issue-Assignee"], - assigneeComment = "", - creatorComment = "", - mergedComment = "", - pullRequestReviewerComment = ""; + let permitComment = "", + title = ["Issue-Assignee"]; + // The mapping between gh handle and comment with a permit url const commentersReward: Record = {}; const prReviewersReward: Record = {}; @@ -380,14 +377,7 @@ export const handleIssueClosed = async ( // CREATOR REWARD HANDLER // Generate permit for user if its not the same id as assignee - if (creatorReward && creatorReward.reward && creatorReward.reward[0].account !== "0x" && creatorReward.userId !== incentivesCalculation.assignee.node_id) { - // const { payoutUrl } = await generatePermit2Signature( - // creatorReward.reward[0].account, - // creatorReward.reward[0].priceInEth, - // incentivesCalculation.issue.node_id, - // creatorReward.userId - // ); - + if (creatorReward && creatorReward.reward && creatorReward.reward[0].account !== "0x") { rewardByUser.push({ account: creatorReward.reward[0].account, priceInEth: creatorReward.reward[0].priceInEth, @@ -397,39 +387,12 @@ export const handleIssueClosed = async ( user: creatorReward.username, priceArray: [creatorReward.reward[0].priceInEth.toString()], }); - - // creatorComment = `#### ${creatorReward.title} Reward \n### [ **${creatorReward.username}: [ CLAIM ${ - // creatorReward.reward[0].priceInEth - // } ${incentivesCalculation.tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n`; - // if (payoutUrl) { - // logger.info(`Permit url generated for creator. reward: ${payoutUrl}`); - // } - // Add amount to assignee if assignee is the creator - } else if ( - creatorReward && - creatorReward.reward && - creatorReward.reward[0].account !== "0x" && - creatorReward.userId === incentivesCalculation.assignee.node_id - ) { - priceInEth = priceInEth.add(creatorReward.reward[0].priceInEth); - title.push("Issue-Creation"); } else if (creatorReward && creatorReward.reward && creatorReward.reward[0].account === "0x") { logger.info(`Skipping to generate a permit url for missing account. fallback: ${creatorReward.fallbackReward}`); } // ASSIGNEE REWARD HANDLER if (assigneeReward && assigneeReward.reward && assigneeReward.reward[0].account !== "0x") { - // const { txData, payoutUrl } = await generatePermit2Signature( - // assigneeReward.reward[0].account, - // assigneeReward.reward[0].priceInEth, - // incentivesCalculation.issue.node_id, - // incentivesCalculation.assignee.node_id - // ); - // const tokenSymbol = await getTokenSymbol(incentivesCalculation.paymentToken, incentivesCalculation.rpc); - // const shortenRecipient = shortenEthAddress(assigneeReward.reward[0].account, `[ CLAIM ${priceInEth} ${tokenSymbol.toUpperCase()} ]`.length); - // logger.info(`Posting a payout url to the issue, url: ${payoutUrl}`); - // assigneeComment = - // `#### ${title} Reward \n### [ **[ CLAIM ${priceInEth} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n` + "```" + shortenRecipient + "```"; const permitComments = incentivesCalculation.comments.filter((content) => { const permitUrlMatches = content.body.match(incentivesCalculation.claimUrlRegex); if (!permitUrlMatches || permitUrlMatches.length < 2) return false; @@ -479,51 +442,55 @@ export const handleIssueClosed = async ( // CREATE PERMIT URL FOR EACH USER for (const reward of rewards) { - const { payoutUrl } = await generatePermit2Signature(reward.account, reward.priceInEth, reward.issueId, reward.userId); + let comment; + const { payoutUrl, txData } = await generatePermit2Signature(reward.account, reward.priceInEth, reward.issueId, reward.userId); if (!reward.user) { logger.info(`Skipping to generate a permit url for missing user. fallback: ${reward.user}`); continue; } - const detailsValue = reward.priceArray.map((price, i) => { - const separateTitle = reward.type[i]?.split("-"); - if (!separateTitle) return { title: "", subtitle: "", value: "" }; - return { title: separateTitle[0], subtitle: separateTitle[1], value: price }; - }); + const detailsValue = reward.priceArray + .map((price, i) => { + const separateTitle = reward.type[i]?.split("-"); + if (!separateTitle) return { title: "", subtitle: "", value: "" }; + return { title: separateTitle[0], subtitle: separateTitle[1], value: price }; + }) + .map((item, i, arr) => { + if (i === 0) return item; + if (item.title === arr[0].title) return { ...item, title: "" }; + return item; + }); const mergedType = reward.type.join(","); const price = `${reward.priceInEth} ${tokenSymbol.toUpperCase()}`; switch (mergedType) { case "Issue-Comments,Review-Reviewer": - const responseOne = createDetailsTable(price, payoutUrl, reward.user, detailsValue); - console.log(responseOne); + comment = createDetailsTable(price, payoutUrl, reward.user, detailsValue); break; case "Issue-Comments": - const responseTwo = createDetailsTable(price, payoutUrl, reward.user, detailsValue); - console.log(responseTwo); + comment = createDetailsTable(price, payoutUrl, reward.user, detailsValue); break; case "Review-Reviewer": - const responseThree = createDetailsTable(price, payoutUrl, reward.user, detailsValue); - console.log(responseThree); + comment = createDetailsTable(price, payoutUrl, reward.user, detailsValue); break; - case "Issue-Assignee,Issue-Creation": - const responseFour = createDetailsTable(price, payoutUrl, reward.user, detailsValue); - console.log(responseFour); + case "Issue-Creation,Issue-Assignee": + comment = createDetailsTable(price, payoutUrl, reward.user, detailsValue); break; case "Issue-Assignee": - const responseFive = createDetailsTable(price, payoutUrl, reward.user, detailsValue); - console.log(responseFive); + comment = createDetailsTable(price, payoutUrl, reward.user, detailsValue); break; case "Issue-Creation": - const responseSix = createDetailsTable(price, payoutUrl, reward.user, detailsValue); - console.log(responseSix); + comment = createDetailsTable(price, payoutUrl, reward.user, detailsValue); break; default: break; } + await savePermitToDB(incentivesCalculation.assignee.id, txData); + permitComment += comment; + logger.info(`Permit url generated for contributors. reward: ${JSON.stringify(commentersReward)}`); logger.info(`Skipping to generate a permit url for missing accounts. fallback: ${JSON.stringify(conversationRewards.fallbackReward)}`); @@ -531,16 +498,10 @@ export const handleIssueClosed = async ( logger.info(`Skipping to generate a permit url for missing accounts. fallback: ${JSON.stringify(pullRequestReviewersReward.fallbackReward)}`); } - //await savePermitToDB(incentivesCalculation.assignee.id, txData); - - if (commentersComment && !isEmpty(commentersReward)) await addCommentToIssue(commentersComment, issueNumber); - if (creatorComment) await addCommentToIssue(creatorComment, issueNumber); - if (pullRequestReviewerComment && !isEmpty(prReviewersReward)) await addCommentToIssue(pullRequestReviewerComment, issueNumber); - if (mergedComment) await addCommentToIssue(mergedComment, issueNumber); - if (assigneeComment) await addCommentToIssue(assigneeComment + comments.promotionComment, issueNumber); + if (permitComment) await addCommentToIssue(permitComment.trim() + comments.promotionComment, issueNumber); await deleteLabel(incentivesCalculation.issueDetailed.priceLabel); - //await addLabelToIssue("Permitted"); + await addLabelToIssue("Permitted"); return { error: "" }; }; diff --git a/src/helpers/comment.ts b/src/helpers/comment.ts index 3ac8aa909..35ef57be0 100644 --- a/src/helpers/comment.ts +++ b/src/helpers/comment.ts @@ -72,13 +72,11 @@ export const createDetailsTable = (
 @${username}
- - - - ${tableRows} - -
-
+ + + ${tableRows} + +
`; From f308ef7bf3a1d861cddabc836bc43714731616e6 Mon Sep 17 00:00:00 2001 From: DevPanther Date: Thu, 21 Sep 2023 21:46:41 +0100 Subject: [PATCH 05/23] chore: check for permit existence before creation --- src/handlers/payout/action.ts | 22 +++++++++++++--------- src/handlers/payout/post.ts | 26 -------------------------- 2 files changed, 13 insertions(+), 35 deletions(-) diff --git a/src/handlers/payout/action.ts b/src/handlers/payout/action.ts index 92da597dc..6a8837f3a 100644 --- a/src/handlers/payout/action.ts +++ b/src/handlers/payout/action.ts @@ -220,6 +220,13 @@ export const incentivesCalculation = async (): Promise content.body.includes("https://pay.ubq.fi?claim=") && content.user.type == UserType.Bot); + + if (permitComments.length > 0) { + logger.info(`skip to generate a permit url because it has been already posted`); + throw new Error(`skip to generate a permit url because it has been already posted`); + } + const tokenSymbol = await getTokenSymbol(paymentToken, rpc); return { @@ -323,10 +330,6 @@ export const handleIssueClosed = async ( let permitComment = "", title = ["Issue-Assignee"]; - // The mapping between gh handle and comment with a permit url - const commentersReward: Record = {}; - const prReviewersReward: Record = {}; - // Rewards by user const rewardByUser: RewardByUser[] = []; @@ -438,7 +441,10 @@ export const handleIssueClosed = async ( return acc; }, [] as RewardByUser[]); - const tokenSymbol = await getTokenSymbol(incentivesCalculation.paymentToken, incentivesCalculation.rpc); + // sort rewards by price + rewards.sort((a, b) => { + return new Decimal(b.priceInEth).cmp(new Decimal(a.priceInEth)); + }); // CREATE PERMIT URL FOR EACH USER for (const reward of rewards) { @@ -456,6 +462,7 @@ export const handleIssueClosed = async ( if (!separateTitle) return { title: "", subtitle: "", value: "" }; return { title: separateTitle[0], subtitle: separateTitle[1], value: price }; }) + // remove title if it's the same as the first one .map((item, i, arr) => { if (i === 0) return item; if (item.title === arr[0].title) return { ...item, title: "" }; @@ -463,7 +470,7 @@ export const handleIssueClosed = async ( }); const mergedType = reward.type.join(","); - const price = `${reward.priceInEth} ${tokenSymbol.toUpperCase()}`; + const price = `${reward.priceInEth} ${incentivesCalculation.tokenSymbol.toUpperCase()}`; switch (mergedType) { case "Issue-Comments,Review-Reviewer": @@ -491,10 +498,7 @@ export const handleIssueClosed = async ( await savePermitToDB(incentivesCalculation.assignee.id, txData); permitComment += comment; - logger.info(`Permit url generated for contributors. reward: ${JSON.stringify(commentersReward)}`); logger.info(`Skipping to generate a permit url for missing accounts. fallback: ${JSON.stringify(conversationRewards.fallbackReward)}`); - - logger.info(`Permit url generated for pull request reviewers. reward: ${JSON.stringify(prReviewersReward)}`); logger.info(`Skipping to generate a permit url for missing accounts. fallback: ${JSON.stringify(pullRequestReviewersReward.fallbackReward)}`); } diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index 48d2793ac..3dfabcd8f 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -32,14 +32,6 @@ export const calculateIssueConversationReward = async (calculateIncentives: Ince const payload = context.payload as Payload; const issue = payload.issue; - const permitComments = calculateIncentives.comments.filter( - (content) => content.body.includes(title) && content.body.includes("https://pay.ubq.fi?claim=") && content.user.type == UserType.Bot - ); - if (permitComments.length > 0) { - logger.info(`incentivizeComments: skip to generate a permit url because it has been already posted`); - return { error: `incentivizeComments: skip to generate a permit url because it has been already posted` }; - } - const assignees = issue?.assignees ?? []; const assignee = assignees.length > 0 ? assignees[0] : undefined; if (!assignee) { @@ -112,15 +104,6 @@ export const calculateIssueCreatorReward = async (incentivesCalculation: Incenti return { error: `incentivizeCreatorComment: its not a bounty` }; } - const comments = await getAllIssueComments(incentivesCalculation.issue.number); - const permitComments = comments.filter( - (content) => content.body.includes(title) && content.body.includes("https://pay.ubq.fi?claim=") && content.user.type == UserType.Bot - ); - if (permitComments.length > 0) { - logger.info(`incentivizeCreatorComment: skip to generate a permit url because it has been already posted`); - return { error: `incentivizeCreatorComment: skip to generate a permit url because it has been already posted` }; - } - const assignees = incentivesCalculation.issue.assignees ?? []; const assignee = assignees.length > 0 ? assignees[0] : undefined; if (!assignee) { @@ -187,15 +170,6 @@ export const calculatePullRequestReviewsReward = async (incentivesCalculation: I return { error: `calculatePullRequestReviewsReward: No linked pull requests found` }; } - const comments = await getAllIssueComments(incentivesCalculation.issue.number); - const permitComments = comments.filter( - (content) => content.body.includes(title) && content.body.includes("https://pay.ubq.fi?claim=") && content.user.type == UserType.Bot - ); - if (permitComments.length > 0) { - logger.info(`calculatePullRequestReviewsReward: skip to generate a permit url because it has been already posted`); - return { error: `calculatePullRequestReviewsReward: skip to generate a permit url because it has been already posted` }; - } - const assignees = incentivesCalculation.issue?.assignees ?? []; const assignee = assignees.length > 0 ? assignees[0] : undefined; if (!assignee) { From 8b51791a78f47ea388056fecae6051352a39421c Mon Sep 17 00:00:00 2001 From: DevPanther Date: Thu, 21 Sep 2023 22:25:16 +0100 Subject: [PATCH 06/23] chore: add multiplier check --- src/handlers/payout/action.ts | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/handlers/payout/action.ts b/src/handlers/payout/action.ts index 6a8837f3a..1ff8a1583 100644 --- a/src/handlers/payout/action.ts +++ b/src/handlers/payout/action.ts @@ -23,6 +23,7 @@ import { isParentIssue } from "../pricing"; import { RewardsResponse } from "../comment"; export interface IncentivesCalculationResult { + id: number; paymentToken: string; rpc: string; networkId: number; @@ -230,6 +231,7 @@ export const incentivesCalculation = async (): Promise { const separateTitle = reward.type[i]?.split("-"); if (!separateTitle) return { title: "", subtitle: "", value: "" }; @@ -469,6 +470,21 @@ export const handleIssueClosed = async ( return item; }); + const { reason, value } = await getWalletMultiplier(reward.user, incentivesCalculation.id?.toString()); + + // if reason is not "", then add multiplier to detailsValue and multiply the price + if (reason) { + detailsValue.push({ title: "Multiplier", subtitle: "Amount", value: value.toString() }); + detailsValue.push({ title: "", subtitle: "Reason", value: reason }); + + const multiplier = new Decimal(value); + const price = new Decimal(reward.priceInEth); + // add multiplier to the price + reward.priceInEth = price.mul(multiplier); + } + + const { payoutUrl, txData } = await generatePermit2Signature(reward.account, reward.priceInEth, reward.issueId, reward.userId); + const mergedType = reward.type.join(","); const price = `${reward.priceInEth} ${incentivesCalculation.tokenSymbol.toUpperCase()}`; From 6f946531193f6e3f5376b71ceb01b16cd8df7633 Mon Sep 17 00:00:00 2001 From: DevPanther Date: Thu, 21 Sep 2023 22:30:17 +0100 Subject: [PATCH 07/23] chore: add permits to db per user id --- src/handlers/payout/action.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/handlers/payout/action.ts b/src/handlers/payout/action.ts index 1ff8a1583..f63033a48 100644 --- a/src/handlers/payout/action.ts +++ b/src/handlers/payout/action.ts @@ -452,7 +452,7 @@ export const handleIssueClosed = async ( for (const reward of rewards) { let comment; - if (!reward.user) { + if (!reward.user || !reward.userId) { logger.info(`Skipping to generate a permit url for missing user. fallback: ${reward.user}`); continue; } @@ -511,7 +511,7 @@ export const handleIssueClosed = async ( break; } - await savePermitToDB(incentivesCalculation.assignee.id, txData); + await savePermitToDB(Number(reward.userId), txData); permitComment += comment; logger.info(`Skipping to generate a permit url for missing accounts. fallback: ${JSON.stringify(conversationRewards.fallbackReward)}`); From e2347da3e80d925d4cfb5ba1658bb9f878ec9e8c Mon Sep 17 00:00:00 2001 From: DevPanther Date: Thu, 21 Sep 2023 22:50:20 +0100 Subject: [PATCH 08/23] chore: switch node id to id for permit to db support --- src/handlers/comment/handlers/index.ts | 4 ++-- src/handlers/payout/action.ts | 8 ++++---- src/handlers/payout/post.ts | 18 +++++++++--------- 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/src/handlers/comment/handlers/index.ts b/src/handlers/comment/handlers/index.ts index 490b0b934..757b6cd54 100644 --- a/src/handlers/comment/handlers/index.ts +++ b/src/handlers/comment/handlers/index.ts @@ -50,14 +50,14 @@ export * from "./ask"; export interface RewardsResponse { error: string | null; title?: string; - userId?: string; + userId?: number; username?: string; reward?: { account: string; priceInEth: Decimal; penaltyAmount: BigNumber; user: string; - userId: string; + userId: number; }[]; fallbackReward?: Record; } diff --git a/src/handlers/payout/action.ts b/src/handlers/payout/action.ts index f63033a48..10401d9f9 100644 --- a/src/handlers/payout/action.ts +++ b/src/handlers/payout/action.ts @@ -51,7 +51,7 @@ export interface IncentivesCalculationResult { export interface RewardByUser { account: string; priceInEth: Decimal; - userId: string | undefined; + userId: number | undefined; issueId: string; type: (string | undefined)[]; user: string | undefined; @@ -304,7 +304,7 @@ export const calculateIssueAssigneeReward = async (incentivesCalculation: Incent return { title: "Issue-Assignee", error: "", - userId: incentivesCalculation.assignee.node_id, + userId: incentivesCalculation.assignee.id, username: assigneeLogin, reward: [ { @@ -312,7 +312,7 @@ export const calculateIssueAssigneeReward = async (incentivesCalculation: Incent penaltyAmount, account: account || "0x", user: "", - userId: "", + userId: incentivesCalculation.assignee.id, }, ], }; @@ -483,7 +483,7 @@ export const handleIssueClosed = async ( reward.priceInEth = price.mul(multiplier); } - const { payoutUrl, txData } = await generatePermit2Signature(reward.account, reward.priceInEth, reward.issueId, reward.userId); + const { payoutUrl, txData } = await generatePermit2Signature(reward.account, reward.priceInEth, reward.issueId, reward.userId?.toString()); const mergedType = reward.type.join(","); const price = `${reward.priceInEth} ${incentivesCalculation.tokenSymbol.toUpperCase()}`; diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index 3dfabcd8f..aedddadde 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -41,7 +41,7 @@ export const calculateIssueConversationReward = async (calculateIncentives: Ince const issueComments = await getAllIssueComments(calculateIncentives.issue.number, "full"); logger.info(`Getting the issue comments done. comments: ${JSON.stringify(issueComments)}`); - const issueCommentsByUser: Record = {}; + const issueCommentsByUser: Record = {}; for (const issueComment of issueComments) { const user = issueComment.user; if (user.type == UserType.Bot || user.login == assignee.login) continue; @@ -57,7 +57,7 @@ export const calculateIssueConversationReward = async (calculateIncentives: Ince // Store the comment along with user's login and node_id if (!issueCommentsByUser[user.login]) { - issueCommentsByUser[user.login] = { id: user.node_id, comments: [] }; + issueCommentsByUser[user.login] = { id: user.id, comments: [] }; } issueCommentsByUser[user.login].comments.push(issueComment.body_html); } @@ -67,7 +67,7 @@ export const calculateIssueConversationReward = async (calculateIncentives: Ince const fallbackReward: Record = {}; // array of awaiting permits to generate - const reward: { account: string; priceInEth: Decimal; userId: string; user: string; penaltyAmount: BigNumber }[] = []; + const reward: { account: string; priceInEth: Decimal; userId: number; user: string; penaltyAmount: BigNumber }[] = []; for (const user of Object.keys(issueCommentsByUser)) { const commentsByUser = issueCommentsByUser[user]; @@ -138,13 +138,13 @@ export const calculateIssueCreatorReward = async (incentivesCalculation: Incenti return { error: "", title, - userId: creator.node_id, + userId: creator.id, username: creator.login, reward: [ { priceInEth: result?.amountInETH ?? new Decimal(0), account: result?.account, - userId: "", + userId: creator.id, user: "", penaltyAmount: BigNumber.from(0), }, @@ -180,7 +180,7 @@ export const calculatePullRequestReviewsReward = async (incentivesCalculation: I const prReviews = await getAllPullRequestReviews(context, latestLinkedPullRequest.number, "full"); const prComments = await getAllIssueComments(latestLinkedPullRequest.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; @@ -190,7 +190,7 @@ export const calculatePullRequestReviewsReward = async (incentivesCalculation: I continue; } if (!prReviewsByUser[user.login]) { - prReviewsByUser[user.login] = { id: user.node_id, comments: [] }; + prReviewsByUser[user.login] = { id: user.id, comments: [] }; } prReviewsByUser[user.login].comments.push(review.body_html); } @@ -204,7 +204,7 @@ export const calculatePullRequestReviewsReward = async (incentivesCalculation: I continue; } if (!prReviewsByUser[user.login]) { - prReviewsByUser[user.login] = { id: user.node_id, comments: [] }; + prReviewsByUser[user.login] = { id: user.id, comments: [] }; } prReviewsByUser[user.login].comments.push(comment.body_html); } @@ -212,7 +212,7 @@ export const calculatePullRequestReviewsReward = async (incentivesCalculation: I logger.info(`calculatePullRequestReviewsReward: Filtering by the user type done. commentsByUser: ${JSON.stringify(prReviewsByUser)}`); // array of awaiting permits to generate - const reward: { account: string; priceInEth: Decimal; userId: string; user: string; penaltyAmount: BigNumber }[] = []; + const reward: { account: string; priceInEth: Decimal; userId: number; user: string; penaltyAmount: BigNumber }[] = []; // The mapping between gh handle and amount in ETH const fallbackReward: Record = {}; From b251b9fe225bc89441f35c13c3100a8d1a07f593 Mon Sep 17 00:00:00 2001 From: DevPanther Date: Thu, 21 Sep 2023 22:51:33 +0100 Subject: [PATCH 09/23] chore: fix linting --- src/handlers/payout/action.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/handlers/payout/action.ts b/src/handlers/payout/action.ts index 10401d9f9..f407677fe 100644 --- a/src/handlers/payout/action.ts +++ b/src/handlers/payout/action.ts @@ -329,16 +329,17 @@ export const handleIssueClosed = async ( const { comments } = getBotConfig(); const issueNumber = incentivesCalculation.issue.number; - let permitComment = "", - title = ["Issue-Assignee"]; + let permitComment = ""; + const title = ["Issue-Assignee"]; // Rewards by user const rewardByUser: RewardByUser[] = []; // ASSIGNEE REWARD PRICE PROCESSOR - let priceInEth = new Decimal(incentivesCalculation.issueDetailed.priceLabel.substring(7, incentivesCalculation.issueDetailed.priceLabel.length - 4)).mul( + const priceInEth = new Decimal(incentivesCalculation.issueDetailed.priceLabel.substring(7, incentivesCalculation.issueDetailed.priceLabel.length - 4)).mul( incentivesCalculation.multiplier ); + if (priceInEth.gt(incentivesCalculation.paymentPermitMaxPrice)) { logger.info("Skipping to proceed the payment because bounty payout is higher than paymentPermitMaxPrice"); return { error: `Permit generation skipped since issue's bounty is higher than ${incentivesCalculation.paymentPermitMaxPrice}` }; @@ -457,7 +458,7 @@ export const handleIssueClosed = async ( continue; } - let detailsValue = reward.priceArray + const detailsValue = reward.priceArray .map((price, i) => { const separateTitle = reward.type[i]?.split("-"); if (!separateTitle) return { title: "", subtitle: "", value: "" }; From 71bc0d705f14e3f11d0fed7e43956dbbafe86238 Mon Sep 17 00:00:00 2001 From: Seprintour Date: Tue, 26 Sep 2023 07:16:07 +0100 Subject: [PATCH 10/23] chore: setting up necessary env --- .env.example | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.env.example b/.env.example index 819c46da0..bd223ed6e 100644 --- a/.env.example +++ b/.env.example @@ -21,3 +21,8 @@ CHATGPT_USER_PROMPT_FOR_MEASURE_SIMILARITY='I have two github issues and I need SIMILARITY_THRESHOLD=80 MEASURE_SIMILARITY_AI_TEMPERATURE=0 IMPORTANT_WORDS_AI_TEMPERATURE=0 + +# Telegram Log Notification Envs +LOG_WEBHOOK_SECRET= # Random Secret, Shared between the telegram bot and the sender +LOG_WEBHOOK_GROUP_ID= # Group Id, ex: -100124234325 +LOG_WEBHOOK_TOPIC_ID= # Topic Id (Optional), Only provide if group is a topic and you're not using General \ No newline at end of file From 81ec4855e361d6236d32c36dc151ce04f40ffc2e Mon Sep 17 00:00:00 2001 From: Seprintour Date: Tue, 26 Sep 2023 07:24:33 +0100 Subject: [PATCH 11/23] chore: setting up bindings --- src/bindings/config.ts | 10 ++++++++++ src/types/config.ts | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/src/bindings/config.ts b/src/bindings/config.ts index 49a3e4fc3..fb99f96a7 100644 --- a/src/bindings/config.ts +++ b/src/bindings/config.ts @@ -84,6 +84,12 @@ export const loadConfig = async (context: Context): Promise => { token: process.env.TELEGRAM_BOT_TOKEN ?? "", delay: process.env.TELEGRAM_BOT_DELAY ? Number(process.env.TELEGRAM_BOT_DELAY) : DEFAULT_BOT_DELAY, }, + logNotification: { + secret: process.env.LOG_WEBHOOK_SECRET || "", + groupId: Number(process.env.LOG_WEBHOOK_GROUP_ID) || 0, + topicId: Number(process.env.LOG_WEBHOOK_TOPIC_ID) || 0, + enabled: true, + }, mode: { paymentPermitMaxPrice: paymentPermitMaxPrice, disableAnalytics: disableAnalytics, @@ -114,6 +120,10 @@ export const loadConfig = async (context: Context): Promise => { botConfig.mode.paymentPermitMaxPrice = 0; } + if (botConfig.logNotification.secret == "" || botConfig.logNotification.groupId == 0) { + botConfig.logNotification.enabled = false; + } + const validate = ajv.compile(BotConfigSchema); const valid = validate(botConfig); if (!valid) { diff --git a/src/types/config.ts b/src/types/config.ts index cd6d6d109..f641772b1 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -63,6 +63,13 @@ export const TelegramBotConfigSchema = Type.Object({ delay: Type.Number(), }); +export const LogNotificationSchema = Type.Object({ + secret: Type.String(), + groupId: Type.Number(), + topicId: Type.Number(), + enabled: Type.Boolean(), +}); + export const PayoutConfigSchema = Type.Object({ networkId: Type.Number(), rpc: Type.String(), @@ -139,6 +146,7 @@ export const BotConfigSchema = Type.Object({ unassign: UnassignConfigSchema, supabase: SupabaseConfigSchema, telegram: TelegramBotConfigSchema, + logNotification: LogNotificationSchema, mode: ModeSchema, assign: AssignSchema, sodium: SodiumSchema, From 2651c0a93b79cf2ebc10d081a9ea518d140d4362 Mon Sep 17 00:00:00 2001 From: Seprintour Date: Tue, 26 Sep 2023 07:53:19 +0100 Subject: [PATCH 12/23] chore: setup logger function --- .env.example | 1 + package.json | 1 + src/adapters/supabase/helpers/log.ts | 50 ++++++++++++++++++++++++++-- src/bindings/config.ts | 3 +- src/bindings/event.ts | 3 +- src/types/config.ts | 3 ++ yarn.lock | 2 +- 7 files changed, 57 insertions(+), 6 deletions(-) diff --git a/.env.example b/.env.example index bd223ed6e..e84e78a7c 100644 --- a/.env.example +++ b/.env.example @@ -23,6 +23,7 @@ MEASURE_SIMILARITY_AI_TEMPERATURE=0 IMPORTANT_WORDS_AI_TEMPERATURE=0 # Telegram Log Notification Envs +LOG_WEBHOOK_BOT_URL= # URL of cloudflare worker LOG_WEBHOOK_SECRET= # Random Secret, Shared between the telegram bot and the sender LOG_WEBHOOK_GROUP_ID= # Group Id, ex: -100124234325 LOG_WEBHOOK_TOPIC_ID= # Topic Id (Optional), Only provide if group is a topic and you're not using General \ No newline at end of file diff --git a/package.json b/package.json index 3ce7f9102..ea4f2470f 100644 --- a/package.json +++ b/package.json @@ -49,6 +49,7 @@ "husky": "^8.0.2", "jimp": "^0.22.4", "js-yaml": "^4.1.0", + "jsonwebtoken": "^9.0.2", "libsodium-wrappers": "^0.7.11", "lint-staged": "^13.1.0", "lodash": "^4.17.21", diff --git a/src/adapters/supabase/helpers/log.ts b/src/adapters/supabase/helpers/log.ts index 51a16f5b2..ff2943e83 100644 --- a/src/adapters/supabase/helpers/log.ts +++ b/src/adapters/supabase/helpers/log.ts @@ -1,7 +1,8 @@ +import axios from "axios"; import { getAdapters, getBotContext, Logger } from "../../../bindings"; -import { Payload, LogLevel } from "../../../types"; +import { Payload, LogLevel, LogNotification } from "../../../types"; import { getOrgAndRepoFromPath } from "../../../utils/private"; - +import jwt from "jsonwebtoken"; interface Log { repo: string | null; org: string | null; @@ -43,13 +44,15 @@ export class GitHubLogger implements Logger { private retryDelay = 1000; // Delay between retries in milliseconds private throttleCount = 0; private retryLimit = 0; // Retries disabled by default + private logNotification; - constructor(app: string, logEnvironment: string, maxLevel: LogLevel, retryLimit: number) { + constructor(app: string, logEnvironment: string, maxLevel: LogLevel, retryLimit: number, logNotification: LogNotification) { this.app = app; this.logEnvironment = logEnvironment; this.maxLevel = getNumericLevel(maxLevel); this.retryLimit = retryLimit; this.supabase = getAdapters().supabase; + this.logNotification = logNotification; } async sendLogsToSupabase({ repo, org, commentId, issueNumber, logMessage, level, timestamp }: Log) { @@ -80,6 +83,45 @@ export class GitHubLogger implements Logger { } } + async sendDataWithJwt(message: string | object, errorPayload?: string | object) { + try { + if (!this.logNotification.enabled) { + throw new Error("Telegram Log Notification is disabled, please check that url, secret and group is provided"); + } + + if (typeof message === "object") { + message = JSON.stringify(message); + } + + if (errorPayload && typeof errorPayload === "object") { + errorPayload = JSON.stringify(errorPayload); + } + + const errorMessage = `${message}${errorPayload ? " - " + errorPayload : ""}`; + + // Step 1: Sign a JWT with the provided parameter + const jwtToken = jwt.sign( + { + group: this.logNotification.groupId, + topic: this.logNotification.topicId, + msg: errorMessage, + }, + this.logNotification.secret + ); + + const apiUrl = this.logNotification.url; + const headers = { + Authorization: `${jwtToken}`, + }; + + const response = await axios.get(apiUrl, { headers }); + + console.log("Log Notification API Response:", response.data); + } catch (error) { + console.error("Log Notification Error:", error); + } + } + async retryLog(log: Log, retryCount = 0) { if (retryCount >= this.retryLimit) { console.error("Max retry limit reached for log:", log); @@ -169,6 +211,7 @@ export class GitHubLogger implements Logger { warn(message: string | object, errorPayload?: string | object) { this.save(message, LogLevel.WARN, errorPayload); + this.sendDataWithJwt(message, errorPayload); } debug(message: string | object, errorPayload?: string | object) { @@ -177,6 +220,7 @@ export class GitHubLogger implements Logger { error(message: string | object, errorPayload?: string | object) { this.save(message, LogLevel.ERROR, errorPayload); + this.sendDataWithJwt(message, errorPayload); } async get() { diff --git a/src/bindings/config.ts b/src/bindings/config.ts index fb99f96a7..87985060d 100644 --- a/src/bindings/config.ts +++ b/src/bindings/config.ts @@ -85,6 +85,7 @@ export const loadConfig = async (context: Context): Promise => { delay: process.env.TELEGRAM_BOT_DELAY ? Number(process.env.TELEGRAM_BOT_DELAY) : DEFAULT_BOT_DELAY, }, logNotification: { + url: process.env.LOG_WEBHOOK_BOT_URL || "", secret: process.env.LOG_WEBHOOK_SECRET || "", groupId: Number(process.env.LOG_WEBHOOK_GROUP_ID) || 0, topicId: Number(process.env.LOG_WEBHOOK_TOPIC_ID) || 0, @@ -120,7 +121,7 @@ export const loadConfig = async (context: Context): Promise => { botConfig.mode.paymentPermitMaxPrice = 0; } - if (botConfig.logNotification.secret == "" || botConfig.logNotification.groupId == 0) { + if (botConfig.logNotification.secret == "" || botConfig.logNotification.groupId == 0 || botConfig.logNotification.url == "") { botConfig.logNotification.enabled = false; } diff --git a/src/bindings/event.ts b/src/bindings/event.ts index f36641565..e3895541a 100644 --- a/src/bindings/event.ts +++ b/src/bindings/event.ts @@ -55,7 +55,8 @@ export const bindEvents = async (context: Context): Promise => { options.app, botConfig?.log?.logEnvironment ?? "development", botConfig?.log?.level ?? LogLevel.DEBUG, - botConfig?.log?.retryLimit ?? 0 + botConfig?.log?.retryLimit ?? 0, + botConfig.logNotification ); // contributors will see logs in console while on development env if (!logger) { return; diff --git a/src/types/config.ts b/src/types/config.ts index f641772b1..84e2296f8 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -64,12 +64,15 @@ export const TelegramBotConfigSchema = Type.Object({ }); export const LogNotificationSchema = Type.Object({ + url: Type.String(), secret: Type.String(), groupId: Type.Number(), topicId: Type.Number(), enabled: Type.Boolean(), }); +export type LogNotification = Static; + export const PayoutConfigSchema = Type.Object({ networkId: Type.Number(), rpc: Type.String(), diff --git a/yarn.lock b/yarn.lock index 96647fe75..b9d3e2cbf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5941,7 +5941,7 @@ jsonparse@^1.2.0: resolved "https://registry.yarnpkg.com/jsonparse/-/jsonparse-1.3.1.tgz#3f4dae4a91fac315f71062f8521cc239f1366280" integrity sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg== -jsonwebtoken@^9.0.0: +jsonwebtoken@^9.0.0, jsonwebtoken@^9.0.2: version "9.0.2" resolved "https://registry.yarnpkg.com/jsonwebtoken/-/jsonwebtoken-9.0.2.tgz#65ff91f4abef1784697d40952bb1998c504caaf3" integrity sha512-PRp66vJ865SSqOlgqS8hujT5U4AOgMfhrwYIuIhfKaoSCZcirrmASQr8CX7cUg+RMih+hgznrjp99o+W4pJLHQ== From ef1c04c6f9c13f196bd2c5d93ef61e7200996f36 Mon Sep 17 00:00:00 2001 From: Seprintour Date: Tue, 26 Sep 2023 08:29:24 +0100 Subject: [PATCH 13/23] chore: testing notification --- .env.example | 2 +- src/adapters/supabase/helpers/log.ts | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index e84e78a7c..b6102ad3d 100644 --- a/.env.example +++ b/.env.example @@ -23,7 +23,7 @@ MEASURE_SIMILARITY_AI_TEMPERATURE=0 IMPORTANT_WORDS_AI_TEMPERATURE=0 # Telegram Log Notification Envs -LOG_WEBHOOK_BOT_URL= # URL of cloudflare worker +LOG_WEBHOOK_BOT_URL= # URL of cloudflare worker without trailing / LOG_WEBHOOK_SECRET= # Random Secret, Shared between the telegram bot and the sender LOG_WEBHOOK_GROUP_ID= # Group Id, ex: -100124234325 LOG_WEBHOOK_TOPIC_ID= # Topic Id (Optional), Only provide if group is a topic and you're not using General \ No newline at end of file diff --git a/src/adapters/supabase/helpers/log.ts b/src/adapters/supabase/helpers/log.ts index ff2943e83..6a590762e 100644 --- a/src/adapters/supabase/helpers/log.ts +++ b/src/adapters/supabase/helpers/log.ts @@ -106,10 +106,11 @@ export class GitHubLogger implements Logger { topic: this.logNotification.topicId, msg: errorMessage, }, - this.logNotification.secret + this.logNotification.secret, + { noTimestamp: true } ); - const apiUrl = this.logNotification.url; + const apiUrl = `${this.logNotification.url}/sendLogs`; const headers = { Authorization: `${jwtToken}`, }; From 496addcbb81dafefebcaf0ada9b04d3a6f9bdd8d Mon Sep 17 00:00:00 2001 From: Seprintour Date: Tue, 26 Sep 2023 08:57:08 +0100 Subject: [PATCH 14/23] chore: adding context to notification --- src/adapters/supabase/helpers/log.ts | 84 ++++++++++++++++------------ 1 file changed, 49 insertions(+), 35 deletions(-) diff --git a/src/adapters/supabase/helpers/log.ts b/src/adapters/supabase/helpers/log.ts index 6a590762e..b011c3755 100644 --- a/src/adapters/supabase/helpers/log.ts +++ b/src/adapters/supabase/helpers/log.ts @@ -83,44 +83,58 @@ export class GitHubLogger implements Logger { } } - async sendDataWithJwt(message: string | object, errorPayload?: string | object) { - try { - if (!this.logNotification.enabled) { - throw new Error("Telegram Log Notification is disabled, please check that url, secret and group is provided"); - } - - if (typeof message === "object") { - message = JSON.stringify(message); - } - - if (errorPayload && typeof errorPayload === "object") { - errorPayload = JSON.stringify(errorPayload); - } - - const errorMessage = `${message}${errorPayload ? " - " + errorPayload : ""}`; - - // Step 1: Sign a JWT with the provided parameter - const jwtToken = jwt.sign( - { - group: this.logNotification.groupId, - topic: this.logNotification.topicId, - msg: errorMessage, - }, - this.logNotification.secret, - { noTimestamp: true } - ); + private sendDataWithJwt(message: string | object, errorPayload?: string | object) { + const context = getBotContext(); + const payload = context.payload as Payload; - const apiUrl = `${this.logNotification.url}/sendLogs`; - const headers = { - Authorization: `${jwtToken}`, - }; + const { comment, issue, repository } = payload; + const commentId = comment?.id; + const issueNumber = issue?.number; + const repoFullName = repository?.full_name; - const response = await axios.get(apiUrl, { headers }); + const { org, repo } = getOrgAndRepoFromPath(repoFullName); - console.log("Log Notification API Response:", response.data); - } catch (error) { - console.error("Log Notification Error:", error); - } + const issueLink = `https://github.com/${org}/${repo}/issues/${issueNumber}${commentId ? `#issuecomment-${commentId}` : ""}`; + + (async () => { + try { + if (!this.logNotification.enabled) { + throw new Error("Telegram Log Notification is disabled, please check that url, secret and group is provided"); + } + + if (typeof message === "object") { + message = JSON.stringify(message); + } + + if (errorPayload && typeof errorPayload === "object") { + errorPayload = JSON.stringify(errorPayload); + } + + const errorMessage = `\`${message}${errorPayload ? " - " + errorPayload : ""}\`\n\nContext: ${issueLink}`; + + // Step 1: Sign a JWT with the provided parameter + const jwtToken = jwt.sign( + { + group: this.logNotification.groupId, + topic: this.logNotification.topicId, + msg: errorMessage, + }, + this.logNotification.secret, + { noTimestamp: true } + ); + + const apiUrl = `${this.logNotification.url}/sendLogs`; + const headers = { + Authorization: `${jwtToken}`, + }; + + const response = await axios.get(apiUrl, { headers }); + + console.log("Log Notification API Response:", response.data); + } catch (error) { + console.error("Log Notification Error:", error); + } + })(); } async retryLog(log: Log, retryCount = 0) { From baed646837de603f0b9bc7c173219263025f66ba Mon Sep 17 00:00:00 2001 From: Seprintour Date: Tue, 26 Sep 2023 09:04:44 +0100 Subject: [PATCH 15/23] fix: promise async errors --- src/adapters/supabase/helpers/log.ts | 34 +++++++++++++++++++++------- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/adapters/supabase/helpers/log.ts b/src/adapters/supabase/helpers/log.ts index b011c3755..378045b7a 100644 --- a/src/adapters/supabase/helpers/log.ts +++ b/src/adapters/supabase/helpers/log.ts @@ -96,7 +96,7 @@ export class GitHubLogger implements Logger { const issueLink = `https://github.com/${org}/${repo}/issues/${issueNumber}${commentId ? `#issuecomment-${commentId}` : ""}`; - (async () => { + return new Promise((resolve, reject) => { try { if (!this.logNotification.enabled) { throw new Error("Telegram Log Notification is disabled, please check that url, secret and group is provided"); @@ -128,13 +128,19 @@ export class GitHubLogger implements Logger { Authorization: `${jwtToken}`, }; - const response = await axios.get(apiUrl, { headers }); - - console.log("Log Notification API Response:", response.data); + axios + .get(apiUrl, { headers }) + .then((response) => { + resolve(response.data); + }) + .catch((error) => { + reject(error); + }); } catch (error) { - console.error("Log Notification Error:", error); + // Reject the promise with the error + reject(error); } - })(); + }); } async retryLog(log: Log, retryCount = 0) { @@ -226,7 +232,13 @@ export class GitHubLogger implements Logger { warn(message: string | object, errorPayload?: string | object) { this.save(message, LogLevel.WARN, errorPayload); - this.sendDataWithJwt(message, errorPayload); + this.sendDataWithJwt(message, errorPayload) + .then((response) => { + console.log("Log Notification Success:", response); + }) + .catch((error) => { + console.error("Log Notification Error:", error); + }); } debug(message: string | object, errorPayload?: string | object) { @@ -235,7 +247,13 @@ export class GitHubLogger implements Logger { error(message: string | object, errorPayload?: string | object) { this.save(message, LogLevel.ERROR, errorPayload); - this.sendDataWithJwt(message, errorPayload); + this.sendDataWithJwt(message, errorPayload) + .then((response) => { + console.log("Log Notification Success:", response); + }) + .catch((error) => { + console.error("Log Notification Error:", error); + }); } async get() { From d250fd144a157167d7af8b1bbd589ca51362f47e Mon Sep 17 00:00:00 2001 From: Seprintour Date: Tue, 26 Sep 2023 16:11:05 +0100 Subject: [PATCH 16/23] chore: console log to logger --- src/adapters/supabase/helpers/log.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/adapters/supabase/helpers/log.ts b/src/adapters/supabase/helpers/log.ts index 378045b7a..a8ca612da 100644 --- a/src/adapters/supabase/helpers/log.ts +++ b/src/adapters/supabase/helpers/log.ts @@ -234,10 +234,10 @@ export class GitHubLogger implements Logger { this.save(message, LogLevel.WARN, errorPayload); this.sendDataWithJwt(message, errorPayload) .then((response) => { - console.log("Log Notification Success:", response); + this.save(`Log Notification Success: ${response}`, LogLevel.DEBUG, ""); }) .catch((error) => { - console.error("Log Notification Error:", error); + this.save(`Log Notification Error: ${error}`, LogLevel.DEBUG, ""); }); } @@ -249,10 +249,10 @@ export class GitHubLogger implements Logger { this.save(message, LogLevel.ERROR, errorPayload); this.sendDataWithJwt(message, errorPayload) .then((response) => { - console.log("Log Notification Success:", response); + this.save(`Log Notification Success: ${response}`, LogLevel.DEBUG, ""); }) .catch((error) => { - console.error("Log Notification Error:", error); + this.save(`Log Notification Error: ${error}`, LogLevel.DEBUG, ""); }); } From 0d9b5e43c87a08fb2b324c7ec8642a50bb0fb269 Mon Sep 17 00:00:00 2001 From: Seprintour Date: Wed, 27 Sep 2023 03:39:51 +0100 Subject: [PATCH 17/23] chore: throw error to reject --- src/adapters/supabase/helpers/log.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/adapters/supabase/helpers/log.ts b/src/adapters/supabase/helpers/log.ts index a8ca612da..81bbf9d6b 100644 --- a/src/adapters/supabase/helpers/log.ts +++ b/src/adapters/supabase/helpers/log.ts @@ -98,8 +98,8 @@ export class GitHubLogger implements Logger { return new Promise((resolve, reject) => { try { - if (!this.logNotification.enabled) { - throw new Error("Telegram Log Notification is disabled, please check that url, secret and group is provided"); + if (!this.logNotification?.enabled) { + reject("Telegram Log Notification is disabled, please check that url, secret and group is provided"); } if (typeof message === "object") { From 2109b267c19b9e56e75222b65eb46c176e704426 Mon Sep 17 00:00:00 2001 From: DevPanther Date: Wed, 27 Sep 2023 04:04:34 +0100 Subject: [PATCH 18/23] chore: refactor code --- src/handlers/payout/action.ts | 26 +------------------------- 1 file changed, 1 insertion(+), 25 deletions(-) diff --git a/src/handlers/payout/action.ts b/src/handlers/payout/action.ts index f407677fe..aac863d1b 100644 --- a/src/handlers/payout/action.ts +++ b/src/handlers/payout/action.ts @@ -451,8 +451,6 @@ export const handleIssueClosed = async ( // CREATE PERMIT URL FOR EACH USER for (const reward of rewards) { - let comment; - if (!reward.user || !reward.userId) { logger.info(`Skipping to generate a permit url for missing user. fallback: ${reward.user}`); continue; @@ -486,31 +484,9 @@ export const handleIssueClosed = async ( const { payoutUrl, txData } = await generatePermit2Signature(reward.account, reward.priceInEth, reward.issueId, reward.userId?.toString()); - const mergedType = reward.type.join(","); const price = `${reward.priceInEth} ${incentivesCalculation.tokenSymbol.toUpperCase()}`; - switch (mergedType) { - case "Issue-Comments,Review-Reviewer": - comment = createDetailsTable(price, payoutUrl, reward.user, detailsValue); - break; - case "Issue-Comments": - comment = createDetailsTable(price, payoutUrl, reward.user, detailsValue); - break; - case "Review-Reviewer": - comment = createDetailsTable(price, payoutUrl, reward.user, detailsValue); - break; - case "Issue-Creation,Issue-Assignee": - comment = createDetailsTable(price, payoutUrl, reward.user, detailsValue); - break; - case "Issue-Assignee": - comment = createDetailsTable(price, payoutUrl, reward.user, detailsValue); - break; - case "Issue-Creation": - comment = createDetailsTable(price, payoutUrl, reward.user, detailsValue); - break; - default: - break; - } + const comment = createDetailsTable(price, payoutUrl, reward.user, detailsValue); await savePermitToDB(Number(reward.userId), txData); permitComment += comment; From c6f79f50b306d4ba3d68f93fda7c943c3ca6185f Mon Sep 17 00:00:00 2001 From: rndquu <119500907+rndquu@users.noreply.github.com> Date: Thu, 28 Sep 2023 13:13:33 +0300 Subject: [PATCH 19/23] fix(hot): refactor config to kebab case --- .github/ubiquibot-config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ubiquibot-config.yml b/.github/ubiquibot-config.yml index 3658a5818..5b48b5f30 100644 --- a/.github/ubiquibot-config.yml +++ b/.github/ubiquibot-config.yml @@ -1,6 +1,6 @@ -priceMultiplier: 1.5 +price-multiplier: 1.5 # newContributorGreeting: # enabled: true # header: "Thank you for contributing to UbiquiBot! Please be sure to set your wallet address before completing your first bounty so that the automatic payout upon task completion will work for you." # helpMenu: true -# footer: "###### Also please star this repository and [@ubiquity/devpool-directory](https://github.com/ubiquity/devpool-directory/) to show your support. It helps a lot!" \ No newline at end of file +# footer: "###### Also please star this repository and [@ubiquity/devpool-directory](https://github.com/ubiquity/devpool-directory/) to show your support. It helps a lot!" From c7896e234cb7d522a03cec2ba6ddd536696db69b Mon Sep 17 00:00:00 2001 From: DevPanther Date: Sat, 30 Sep 2023 04:59:39 +0100 Subject: [PATCH 20/23] chore: setting up debug table --- src/handlers/comment/handlers/index.ts | 1 + src/handlers/payout/action.ts | 1 + src/handlers/payout/post.ts | 43 ++++++++++++++++++-------- src/helpers/comment.ts | 26 ++++++++++++++++ 4 files changed, 58 insertions(+), 13 deletions(-) diff --git a/src/handlers/comment/handlers/index.ts b/src/handlers/comment/handlers/index.ts index 00890f271..7e2cb6716 100644 --- a/src/handlers/comment/handlers/index.ts +++ b/src/handlers/comment/handlers/index.ts @@ -60,6 +60,7 @@ export interface RewardsResponse { penaltyAmount: BigNumber; user: string; userId: number; + debug: Record; }[]; fallbackReward?: Record; } diff --git a/src/handlers/payout/action.ts b/src/handlers/payout/action.ts index 40a8431c5..e095da752 100644 --- a/src/handlers/payout/action.ts +++ b/src/handlers/payout/action.ts @@ -324,6 +324,7 @@ export const calculateIssueAssigneeReward = async (incentivesCalculation: Incent account: account || "0x", user: "", userId: incentivesCalculation.assignee.id, + debug: {}, }, ], }; diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index aedddadde..bb2e715c5 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -67,25 +67,25 @@ export const calculateIssueConversationReward = async (calculateIncentives: Ince const fallbackReward: Record = {}; // array of awaiting permits to generate - const reward: { account: string; priceInEth: Decimal; userId: number; user: string; penaltyAmount: BigNumber }[] = []; + const reward: { account: string; priceInEth: Decimal; userId: number; user: string; penaltyAmount: BigNumber; debug: Record }[] = []; for (const user of Object.keys(issueCommentsByUser)) { const commentsByUser = issueCommentsByUser[user]; const commentsByNode = await parseComments(commentsByUser.comments, ItemsToExclude); const rewardValue = calculateRewardValue(commentsByNode, calculateIncentives.incentives); - if (rewardValue.equals(0)) { + if (rewardValue.sum.equals(0)) { logger.info(`Skipping to generate a permit url because the reward value is 0. user: ${user}`); continue; } logger.debug(`Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`); const account = await getWalletAddress(user); - const priceInEth = rewardValue.mul(calculateIncentives.baseMultiplier); + const priceInEth = rewardValue.sum.mul(calculateIncentives.baseMultiplier); if (priceInEth.gt(calculateIncentives.paymentPermitMaxPrice)) { logger.info(`Skipping comment reward for user ${user} because reward is higher than payment permit max price`); continue; } if (account) { - reward.push({ account, priceInEth, userId: commentsByUser.id, user, penaltyAmount: BigNumber.from(0) }); + reward.push({ account, priceInEth, userId: commentsByUser.id, user, penaltyAmount: BigNumber.from(0), debug: rewardValue.rewardSumByType }); } else { fallbackReward[user] = priceInEth; } @@ -147,6 +147,7 @@ export const calculateIssueCreatorReward = async (incentivesCalculation: Incenti userId: creator.id, user: "", penaltyAmount: BigNumber.from(0), + debug: {}, }, ], }; @@ -212,7 +213,7 @@ export const calculatePullRequestReviewsReward = async (incentivesCalculation: I logger.info(`calculatePullRequestReviewsReward: Filtering by the user type done. commentsByUser: ${JSON.stringify(prReviewsByUser)}`); // array of awaiting permits to generate - const reward: { account: string; priceInEth: Decimal; userId: number; user: string; penaltyAmount: BigNumber }[] = []; + const reward: { account: string; priceInEth: Decimal; userId: number; user: string; penaltyAmount: BigNumber; debug: Record }[] = []; // The mapping between gh handle and amount in ETH const fallbackReward: Record = {}; @@ -221,20 +222,20 @@ export const calculatePullRequestReviewsReward = async (incentivesCalculation: I const commentByUser = prReviewsByUser[user]; const commentsByNode = await parseComments(commentByUser.comments, ItemsToExclude); const rewardValue = calculateRewardValue(commentsByNode, incentivesCalculation.incentives); - if (rewardValue.equals(0)) { + if (rewardValue.sum.equals(0)) { logger.info(`calculatePullRequestReviewsReward: Skipping to generate a permit url because the reward value is 0. user: ${user}`); continue; } logger.info(`calculatePullRequestReviewsReward: Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`); const account = await getWalletAddress(user); - const priceInEth = rewardValue.mul(incentivesCalculation.baseMultiplier); + const priceInEth = rewardValue.sum.mul(incentivesCalculation.baseMultiplier); if (priceInEth.gt(incentivesCalculation.paymentPermitMaxPrice)) { logger.info(`calculatePullRequestReviewsReward: Skipping comment reward for user ${user} because reward is higher than payment permit max price`); continue; } if (account) { - reward.push({ account, priceInEth, userId: commentByUser.id, user, penaltyAmount: BigNumber.from(0) }); + reward.push({ account, priceInEth, userId: commentByUser.id, user, penaltyAmount: BigNumber.from(0), debug: rewardValue.rewardSumByType }); } else { fallbackReward[user] = priceInEth; } @@ -256,13 +257,13 @@ const generatePermitForComments = async ( const logger = getLogger(); const commentsByNode = await parseComments(comments, ItemsToExclude); const rewardValue = calculateRewardValue(commentsByNode, incentives); - if (rewardValue.equals(0)) { + if (rewardValue.sum.equals(0)) { logger.info(`No reward for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`); return; } logger.debug(`Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`); const account = await getWalletAddress(user); - const amountInETH = rewardValue.mul(multiplier); + const amountInETH = rewardValue.sum.mul(multiplier); if (amountInETH.gt(paymentPermitMaxPrice)) { logger.info(`Skipping issue creator reward for user ${user} because reward is higher than payment permit max price`); return; @@ -280,18 +281,27 @@ const generatePermitForComments = async ( * @param incentives - The basic price table for reward calculation * @returns - The reward value */ -const calculateRewardValue = (comments: Record, incentives: Incentives): Decimal => { +const calculateRewardValue = (comments: Record, incentives: Incentives): { sum: Decimal; rewardSumByType: Record } => { let sum = new Decimal(0); + const sumPerType: Record = {}; + for (const key of Object.keys(comments)) { const value = comments[key]; + // Initialize the sum for this key if it doesn't exist + if (!sumPerType[key]) { + sumPerType[key] = 0; + } + // if it's a text node calculate word count and multiply with the reward value if (key == "#text") { if (!incentives.comment.totals.word) { continue; } const wordReward = new Decimal(incentives.comment.totals.word); - const reward = wordReward.mul(value.map((str) => str.trim().split(" ").length).reduce((totalWords, wordCount) => totalWords + wordCount, 0)); + const wordCount = value.map((str) => str.trim().split(" ").length).reduce((totalWords, wordCount) => totalWords + wordCount, 0); + const reward = wordReward.mul(wordCount); + sumPerType[key] += wordCount; sum = sum.add(reward); } else { if (!incentives.comment.elements[key]) { @@ -299,9 +309,16 @@ const calculateRewardValue = (comments: Record, incentives: In } const rewardValue = new Decimal(incentives.comment.elements[key]); const reward = rewardValue.mul(value.length); + sumPerType[key] += value.length; sum = sum.add(reward); } } - return sum; + // Convert the summed values back to Decimal + const rewardSumByType: Record = {}; + for (const key of Object.keys(sum)) { + rewardSumByType[key] = new Decimal(sumPerType[key]); + } + + return { sum, rewardSumByType }; }; diff --git a/src/helpers/comment.ts b/src/helpers/comment.ts index 35ef57be0..902d414d9 100644 --- a/src/helpers/comment.ts +++ b/src/helpers/comment.ts @@ -85,3 +85,29 @@ export const createDetailsTable = ( return cleanedHtml; }; + +export const generateCollapsibleTable = (data: { element: string; units: string; reward: string }[]) => { + // Check if the data array is empty + if (data.length === 0) { + return "No data to display."; + } + + // Create the table header row + const headerRow = "| element | units | reward |\n| --- | --- | --- |"; + + // Create the table rows from the data array + const tableRows = data.map((item) => `| ${item.element} | ${item.units} | ${item.reward} |`).join("\n"); + + // Create the complete Markdown table + const tableMarkdown = ` +
+ Click to toggle table + +${headerRow} +${tableRows} + +
+ `; + + return tableMarkdown; +}; From 970f4786387fda1ca76beb032d0aec8e28377673 Mon Sep 17 00:00:00 2001 From: DevPanther Date: Sat, 30 Sep 2023 05:46:08 +0100 Subject: [PATCH 21/23] chore: merge collapsible table in permit table --- src/handlers/comment/handlers/index.ts | 2 +- src/handlers/payout/action.ts | 7 ++- src/handlers/payout/post.ts | 58 +++++++++++------- src/helpers/comment.ts | 81 +++++++++++++++++--------- 4 files changed, 99 insertions(+), 49 deletions(-) diff --git a/src/handlers/comment/handlers/index.ts b/src/handlers/comment/handlers/index.ts index 7e2cb6716..4dcbf7f21 100644 --- a/src/handlers/comment/handlers/index.ts +++ b/src/handlers/comment/handlers/index.ts @@ -60,7 +60,7 @@ export interface RewardsResponse { penaltyAmount: BigNumber; user: string; userId: number; - debug: Record; + debug: Record; }[]; fallbackReward?: Record; } diff --git a/src/handlers/payout/action.ts b/src/handlers/payout/action.ts index e095da752..25bf01562 100644 --- a/src/handlers/payout/action.ts +++ b/src/handlers/payout/action.ts @@ -56,6 +56,7 @@ export interface RewardByUser { type: (string | undefined)[]; user: string | undefined; priceArray: string[]; + debug: Record; } /** @@ -370,6 +371,7 @@ export const handleIssueClosed = async ( type: [conversationRewards.title], user: permit.user, priceArray: [permit.priceInEth.toString()], + debug: permit.debug, }); } }); @@ -388,6 +390,7 @@ export const handleIssueClosed = async ( type: [pullRequestReviewersReward.title], user: permit.user, priceArray: [permit.priceInEth.toString()], + debug: permit.debug, }); } }); @@ -404,6 +407,7 @@ export const handleIssueClosed = async ( type: [creatorReward.title], user: creatorReward.username, priceArray: [creatorReward.reward[0].priceInEth.toString()], + debug: creatorReward.reward[0].debug, }); } else if (creatorReward && creatorReward.reward && creatorReward.reward[0].account === "0x") { logger.info(`Skipping to generate a permit url for missing account. fallback: ${creatorReward.fallbackReward}`); @@ -425,6 +429,7 @@ export const handleIssueClosed = async ( type: title, user: assigneeReward.username, priceArray: [assigneeReward.reward[0].priceInEth.toString()], + debug: assigneeReward.reward[0].debug, }); if (permitComments.length > 0) { @@ -498,7 +503,7 @@ export const handleIssueClosed = async ( const price = `${reward.priceInEth} ${incentivesCalculation.tokenSymbol.toUpperCase()}`; - const comment = createDetailsTable(price, payoutUrl, reward.user, detailsValue); + const comment = createDetailsTable(price, payoutUrl, reward.user, detailsValue, reward.debug); await savePermitToDB(Number(reward.userId), txData); permitComment += comment; diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index bb2e715c5..e1ef60e76 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -67,7 +67,14 @@ export const calculateIssueConversationReward = async (calculateIncentives: Ince const fallbackReward: Record = {}; // array of awaiting permits to generate - const reward: { account: string; priceInEth: Decimal; userId: number; user: string; penaltyAmount: BigNumber; debug: Record }[] = []; + const reward: { + account: string; + priceInEth: Decimal; + userId: number; + user: string; + penaltyAmount: BigNumber; + debug: Record; + }[] = []; for (const user of Object.keys(issueCommentsByUser)) { const commentsByUser = issueCommentsByUser[user]; @@ -77,7 +84,7 @@ export const calculateIssueConversationReward = async (calculateIncentives: Ince logger.info(`Skipping to generate a permit url because the reward value is 0. user: ${user}`); continue; } - logger.debug(`Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`); + logger.debug(`Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue.sum}`); const account = await getWalletAddress(user); const priceInEth = rewardValue.sum.mul(calculateIncentives.baseMultiplier); if (priceInEth.gt(calculateIncentives.paymentPermitMaxPrice)) { @@ -85,7 +92,7 @@ export const calculateIssueConversationReward = async (calculateIncentives: Ince continue; } if (account) { - reward.push({ account, priceInEth, userId: commentsByUser.id, user, penaltyAmount: BigNumber.from(0), debug: rewardValue.rewardSumByType }); + reward.push({ account, priceInEth, userId: commentsByUser.id, user, penaltyAmount: BigNumber.from(0), debug: rewardValue.sumByType }); } else { fallbackReward[user] = priceInEth; } @@ -213,7 +220,14 @@ export const calculatePullRequestReviewsReward = async (incentivesCalculation: I logger.info(`calculatePullRequestReviewsReward: Filtering by the user type done. commentsByUser: ${JSON.stringify(prReviewsByUser)}`); // array of awaiting permits to generate - const reward: { account: string; priceInEth: Decimal; userId: number; user: string; penaltyAmount: BigNumber; debug: Record }[] = []; + const reward: { + account: string; + priceInEth: Decimal; + userId: number; + user: string; + penaltyAmount: BigNumber; + debug: Record; + }[] = []; // The mapping between gh handle and amount in ETH const fallbackReward: Record = {}; @@ -226,7 +240,9 @@ export const calculatePullRequestReviewsReward = async (incentivesCalculation: I logger.info(`calculatePullRequestReviewsReward: Skipping to generate a permit url because the reward value is 0. user: ${user}`); continue; } - logger.info(`calculatePullRequestReviewsReward: Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`); + logger.info( + `calculatePullRequestReviewsReward: Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue.sum}` + ); const account = await getWalletAddress(user); const priceInEth = rewardValue.sum.mul(incentivesCalculation.baseMultiplier); if (priceInEth.gt(incentivesCalculation.paymentPermitMaxPrice)) { @@ -235,7 +251,7 @@ export const calculatePullRequestReviewsReward = async (incentivesCalculation: I } if (account) { - reward.push({ account, priceInEth, userId: commentByUser.id, user, penaltyAmount: BigNumber.from(0), debug: rewardValue.rewardSumByType }); + reward.push({ account, priceInEth, userId: commentByUser.id, user, penaltyAmount: BigNumber.from(0), debug: rewardValue.sumByType }); } else { fallbackReward[user] = priceInEth; } @@ -261,7 +277,7 @@ const generatePermitForComments = async ( logger.info(`No reward for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`); return; } - logger.debug(`Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`); + logger.debug(`Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue.sum}`); const account = await getWalletAddress(user); const amountInETH = rewardValue.sum.mul(multiplier); if (amountInETH.gt(paymentPermitMaxPrice)) { @@ -281,16 +297,22 @@ const generatePermitForComments = async ( * @param incentives - The basic price table for reward calculation * @returns - The reward value */ -const calculateRewardValue = (comments: Record, incentives: Incentives): { sum: Decimal; rewardSumByType: Record } => { +const calculateRewardValue = ( + comments: Record, + incentives: Incentives +): { sum: Decimal; sumByType: Record } => { let sum = new Decimal(0); - const sumPerType: Record = {}; + const sumByType: Record = {}; for (const key of Object.keys(comments)) { const value = comments[key]; // Initialize the sum for this key if it doesn't exist - if (!sumPerType[key]) { - sumPerType[key] = 0; + if (!sumByType[key]) { + sumByType[key] = { + count: 0, + reward: new Decimal(0), + }; } // if it's a text node calculate word count and multiply with the reward value @@ -301,7 +323,8 @@ const calculateRewardValue = (comments: Record, incentives: In const wordReward = new Decimal(incentives.comment.totals.word); const wordCount = value.map((str) => str.trim().split(" ").length).reduce((totalWords, wordCount) => totalWords + wordCount, 0); const reward = wordReward.mul(wordCount); - sumPerType[key] += wordCount; + sumByType[key].count += wordCount; + sumByType[key].reward = wordReward; sum = sum.add(reward); } else { if (!incentives.comment.elements[key]) { @@ -309,16 +332,11 @@ const calculateRewardValue = (comments: Record, incentives: In } const rewardValue = new Decimal(incentives.comment.elements[key]); const reward = rewardValue.mul(value.length); - sumPerType[key] += value.length; + sumByType[key].count += value.length; + sumByType[key].reward = rewardValue; sum = sum.add(reward); } } - // Convert the summed values back to Decimal - const rewardSumByType: Record = {}; - for (const key of Object.keys(sum)) { - rewardSumByType[key] = new Decimal(sumPerType[key]); - } - - return { sum, rewardSumByType }; + return { sum, sumByType }; }; diff --git a/src/helpers/comment.ts b/src/helpers/comment.ts index 902d414d9..7be39376c 100644 --- a/src/helpers/comment.ts +++ b/src/helpers/comment.ts @@ -1,3 +1,5 @@ +import Decimal from "decimal.js"; +import { isEmpty } from "lodash"; import * as parse5 from "parse5"; type Node = { @@ -41,12 +43,46 @@ export const parseComments = (comments: string[], itemsToExclude: string[]): Rec return result; }; +export const generateCollapsibleTable = (data: { element: string; units: number; reward: Decimal }[]) => { + // Check if the data array is empty + if (data.length === 0) { + return "No data to display."; + } + + // Create the table header row + const headerRow = "| element | units | reward |\n| --- | --- | --- |"; + + // Create the table rows from the data array + const tableRows = data.map((item) => `| ${item.element} | ${item.units} | ${item.reward} |`).join("\n"); + + // Create the complete Markdown table + const tableMarkdown = ` +
+ Details + +${headerRow} +${tableRows} + +
+ `; + + return tableMarkdown; +}; + export const createDetailsTable = ( amount: string, paymentURL: string, username: string, - values: { title: string; subtitle: string; value: string }[] + values: { title: string; subtitle: string; value: string }[], + debug: Record< + string, + { + count: number; + reward: Decimal; + } + > ): string => { + let collapsibleTable = null; // Generate the table rows based on the values array const tableRows = values .map(({ title, value, subtitle }) => { @@ -61,6 +97,19 @@ export const createDetailsTable = ( }) .join(""); + if (!isEmpty(debug)) { + const data = Object.entries(debug) + .filter(([_, value]) => value.count > 0) + .map(([key, value]) => { + const element = key === "#text" ? "words" : key; + const units = value.count; + const reward = value.reward; + return { element, units, reward }; + }); + + collapsibleTable = generateCollapsibleTable(data); + } + // Construct the complete HTML structure const html = `
@@ -77,37 +126,15 @@ export const createDetailsTable = ( ${tableRows} + ${collapsibleTable ? "COLLAPSIBLE_TABLE_PLACEHOLDER" : ""}
`; // Remove spaces and line breaks from the HTML, ignoring the attributes like
and [ ... ] const cleanedHtml = html.replace(/>\s+<").replace(/[\r\n]+/g, ""); - return cleanedHtml; -}; - -export const generateCollapsibleTable = (data: { element: string; units: string; reward: string }[]) => { - // Check if the data array is empty - if (data.length === 0) { - return "No data to display."; - } - - // Create the table header row - const headerRow = "| element | units | reward |\n| --- | --- | --- |"; - - // Create the table rows from the data array - const tableRows = data.map((item) => `| ${item.element} | ${item.units} | ${item.reward} |`).join("\n"); - - // Create the complete Markdown table - const tableMarkdown = ` -
- Click to toggle table - -${headerRow} -${tableRows} - -
- `; + // Add collapsible table here to avoid compression + const finalHtml = cleanedHtml.replace("COLLAPSIBLE_TABLE_PLACEHOLDER", collapsibleTable || ""); - return tableMarkdown; + return finalHtml; }; From f0b3ac9ddf28113205d712d7f931609ff6598951 Mon Sep 17 00:00:00 2001 From: Paul <41552663+molecula451@users.noreply.github.com> Date: Mon, 2 Oct 2023 08:33:20 -0400 Subject: [PATCH 22/23] chore: update config (#847) * chore: update config * chore: update config * chore: update config * chore: update config * chore: update config * chore: update config * test: update config --- src/bindings/config.ts | 26 ++++++++++----------- src/configs/index.ts | 1 - src/configs/shared.ts | 31 ------------------------- src/configs/ubiquibot-config-default.ts | 6 +++++ src/handlers/comment/handlers/assign.ts | 5 ++-- src/handlers/comment/handlers/help.ts | 9 +++---- src/helpers/label.ts | 8 ++++++- src/helpers/payout.ts | 3 +-- src/types/config.ts | 6 +++++ src/utils/private.ts | 6 +++++ 10 files changed, 46 insertions(+), 55 deletions(-) delete mode 100644 src/configs/shared.ts diff --git a/src/bindings/config.ts b/src/bindings/config.ts index 87985060d..55d0fb8be 100644 --- a/src/bindings/config.ts +++ b/src/bindings/config.ts @@ -1,14 +1,6 @@ import ms from "ms"; import { BotConfig, BotConfigSchema, LogLevel } from "../types"; -import { - DEFAULT_BOT_DELAY, - DEFAULT_DISQUALIFY_TIME, - DEFAULT_FOLLOWUP_TIME, - DEFAULT_PERMIT_BASE_URL, - DEFAULT_TIME_RANGE_FOR_MAX_ISSUE, - DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED, -} from "../configs"; import { getPayoutConfigByNetworkId } from "../helpers"; import { ajv } from "../utils"; import { Context } from "probot"; @@ -37,6 +29,12 @@ export const loadConfig = async (context: Context): Promise => { openAIKey, openAITokenLimit, newContributorGreeting, + timeRangeForMaxIssueEnabled, + timeRangeForMaxIssue, + permitBaseUrl, + botDelay, + followUpTime, + disqualifyTime, } = await getWideConfig(context); const publicKey = await getScalarKey(process.env.X25519_PRIVATE_KEY); @@ -64,17 +62,17 @@ export const loadConfig = async (context: Context): Promise => { rpc: rpc, privateKey: privateKey, paymentToken: paymentToken, - permitBaseUrl: process.env.PERMIT_BASE_URL || DEFAULT_PERMIT_BASE_URL, + permitBaseUrl: process.env.PERMIT_BASE_URL || permitBaseUrl, }, unassign: { timeRangeForMaxIssue: process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE ? Number(process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE) - : DEFAULT_TIME_RANGE_FOR_MAX_ISSUE, + : timeRangeForMaxIssue, timeRangeForMaxIssueEnabled: process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED ? process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED == "true" - : DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED, - followUpTime: ms(process.env.FOLLOW_UP_TIME || DEFAULT_FOLLOWUP_TIME), - disqualifyTime: ms(process.env.DISQUALIFY_TIME || DEFAULT_DISQUALIFY_TIME), + : timeRangeForMaxIssueEnabled, + followUpTime: ms(process.env.FOLLOW_UP_TIME || followUpTime), + disqualifyTime: ms(process.env.DISQUALIFY_TIME || disqualifyTime), }, supabase: { url: process.env.SUPABASE_URL ?? "", @@ -82,7 +80,7 @@ export const loadConfig = async (context: Context): Promise => { }, telegram: { token: process.env.TELEGRAM_BOT_TOKEN ?? "", - delay: process.env.TELEGRAM_BOT_DELAY ? Number(process.env.TELEGRAM_BOT_DELAY) : DEFAULT_BOT_DELAY, + delay: process.env.TELEGRAM_BOT_DELAY ? Number(process.env.TELEGRAM_BOT_DELAY) : botDelay, }, logNotification: { url: process.env.LOG_WEBHOOK_BOT_URL || "", diff --git a/src/configs/index.ts b/src/configs/index.ts index a449ff0d2..6d4bef9de 100644 --- a/src/configs/index.ts +++ b/src/configs/index.ts @@ -1,4 +1,3 @@ -export * from "./shared"; export * from "./strings"; export * from "./abis"; export * from "./ubiquibot-config-default"; diff --git a/src/configs/shared.ts b/src/configs/shared.ts deleted file mode 100644 index 297fdff33..000000000 --- a/src/configs/shared.ts +++ /dev/null @@ -1,31 +0,0 @@ -// cspell:disable -export const COLORS = { - default: "ededed", - price: "1f883d", -}; -// cspell:enable -export const DEFAULT_BOT_DELAY = 100; // 100ms -export const DEFAULT_TIME_RANGE_FOR_MAX_ISSUE = 24; -export const DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED = true; - -export const ASSIGN_COMMAND_ENABLED = true; -/** - * ms('2 days') // 172800000 - * ms('1d') // 86400000 - * ms('10h') // 36000000 - * ms('2.5 hrs') // 9000000 - * ms('2h') // 7200000 - * ms('1m') // 60000 - * ms('5s') // 5000 - * ms('1y') // 31557600000 - * ms('100') // 100 - * ms('-3 days') // -259200000 - * ms('-1h') // -3600000 - * ms('-200') // -200 - */ -export const DEFAULT_FOLLOWUP_TIME = "4 days"; // 4 days -export const DEFAULT_DISQUALIFY_TIME = "7 days"; // 7 days - -export const DEFAULT_NETWORK_ID = 1; // ethereum -export const DEFAULT_RPC_ENDPOINT = "https://rpc-bot.ubq.fi/v1/mainnet"; -export const DEFAULT_PERMIT_BASE_URL = "https://pay.ubq.fi"; diff --git a/src/configs/ubiquibot-config-default.ts b/src/configs/ubiquibot-config-default.ts index 4a989995f..9598ef782 100644 --- a/src/configs/ubiquibot-config-default.ts +++ b/src/configs/ubiquibot-config-default.ts @@ -98,6 +98,12 @@ export const DefaultConfig: MergedConfig = { organization: true, }, staleBountyTime: "0d", + timeRangeForMaxIssue: 24, //24 + timeRangeForMaxIssueEnabled: false, + permitBaseUrl: "https://pay.ubq.fi", + botDelay: 100, // 100ms + followUpTime: "4 days", + disqualifyTime: "7 days", newContributorGreeting: { enabled: true, header: diff --git a/src/handlers/comment/handlers/assign.ts b/src/handlers/comment/handlers/assign.ts index 6a8fabfe4..d4ab030ef 100644 --- a/src/handlers/comment/handlers/assign.ts +++ b/src/handlers/comment/handlers/assign.ts @@ -5,7 +5,7 @@ import { deadLinePrefix } from "../../shared"; import { getWalletAddress, getWalletMultiplier } from "../../../adapters/supabase"; import { tableComment } from "./table"; import { bountyInfo } from "../../wildcard"; -import { ASSIGN_COMMAND_ENABLED, GLOBAL_STRINGS } from "../../../configs"; +import { GLOBAL_STRINGS } from "../../../configs"; import { isParentIssue } from "../../pricing"; export const assign = async (body: string) => { @@ -19,6 +19,7 @@ export const assign = async (body: string) => { const id = organization?.id || repository?.id; // repository?.id as fallback const staleBounty = config.assign.staleBountyTime; + const startEnabled = config.command.find((command) => command.name === "start"); logger.info(`Received '/start' command from user: ${payload.sender.login}, body: ${body}`); const issue = (_payload as Payload).issue; @@ -28,7 +29,7 @@ export const assign = async (body: string) => { return "Skipping '/start' because of no issue instance"; } - if (!ASSIGN_COMMAND_ENABLED) { + if (!startEnabled?.enabled) { logger.info(`Ignore '/start' command from user: ASSIGN_COMMAND_ENABLED config is set false`); return GLOBAL_STRINGS.assignCommandDisabledComment; } diff --git a/src/handlers/comment/handlers/help.ts b/src/handlers/comment/handlers/help.ts index 7e2c11bd4..c2f545049 100644 --- a/src/handlers/comment/handlers/help.ts +++ b/src/handlers/comment/handlers/help.ts @@ -1,6 +1,5 @@ import { userCommands } from "."; -import { getBotContext, getLogger } from "../../../bindings"; -import { ASSIGN_COMMAND_ENABLED } from "../../../configs"; +import { getBotConfig, getBotContext, getLogger } from "../../../bindings"; import { IssueType, Payload } from "../../../types"; import { IssueCommentCommands } from "../commands"; @@ -28,13 +27,15 @@ export const listAvailableCommands = async (body: string) => { }; export const generateHelpMenu = () => { + const config = getBotConfig(); + const startEnabled = config.command.find((command) => command.name === "start"); let helpMenu = "### Available commands\n```"; const commands = userCommands(); commands.map((command) => { // if first command, add a new line if (command.id === commands[0].id) { helpMenu += `\n`; - if (!ASSIGN_COMMAND_ENABLED) return; + if (!startEnabled) return; } helpMenu += `- ${command.id}: ${command.description}`; // if not last command, add a new line (fixes too much space below) @@ -43,6 +44,6 @@ export const generateHelpMenu = () => { } }); - if (!ASSIGN_COMMAND_ENABLED) helpMenu += "```\n***_To assign yourself to an issue, please open a draft pull request that is linked to it._***"; + if (!startEnabled) helpMenu += "```\n***_To assign yourself to an issue, please open a draft pull request that is linked to it._***"; return helpMenu; }; diff --git a/src/helpers/label.ts b/src/helpers/label.ts index a02d6c5eb..7bc3744ec 100644 --- a/src/helpers/label.ts +++ b/src/helpers/label.ts @@ -1,11 +1,17 @@ import { Context } from "probot"; import { getBotConfig, getBotContext, getLogger } from "../bindings"; -import { COLORS } from "../configs"; import { calculateBountyPrice } from "../handlers"; import { Label, Payload } from "../types"; import { deleteLabel } from "./issue"; import { calculateWeight } from "../helpers"; +// cspell:disable +export const COLORS = { + default: "ededed", + price: "1f883d", +}; +// cspell:enable + export const listLabelsForRepo = async (per_page?: number, page?: number): Promise => { const context = getBotContext(); const payload = context.payload as Payload; diff --git a/src/helpers/payout.ts b/src/helpers/payout.ts index 21a8f7c8f..c6f4a06f2 100644 --- a/src/helpers/payout.ts +++ b/src/helpers/payout.ts @@ -12,7 +12,6 @@ */ import { Static } from "@sinclair/typebox"; -import { DEFAULT_RPC_ENDPOINT } from "../configs"; import { PayoutConfigSchema } from "../types"; import { getUserPermission } from "./issue"; import { getBotContext, getLogger } from "../bindings"; @@ -21,7 +20,7 @@ import { getAccessLevel } from "../adapters/supabase"; // available tokens for payouts const PAYMENT_TOKEN_PER_NETWORK: Record = { "1": { - rpc: DEFAULT_RPC_ENDPOINT, + rpc: "https://rpc-bot.ubq.fi/v1/mainnet", token: "0x6B175474E89094C44Da98b954EedeAC495271d0F", // DAI }, "100": { diff --git a/src/types/config.ts b/src/types/config.ts index 84e2296f8..a22683a28 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -236,6 +236,12 @@ export const MergedConfigSchema = Type.Object({ openAITokenLimit: Type.Optional(Type.Number()), staleBountyTime: Type.String(), newContributorGreeting: NewContributorGreetingSchema, + timeRangeForMaxIssue: Type.Number(), + timeRangeForMaxIssueEnabled: Type.Boolean(), + permitBaseUrl: Type.String(), + botDelay: Type.Number(), + followUpTime: Type.String(), + disqualifyTime: Type.String(), }); export type MergedConfig = Static; diff --git a/src/utils/private.ts b/src/utils/private.ts index 9568f45dd..48c498f9a 100644 --- a/src/utils/private.ts +++ b/src/utils/private.ts @@ -162,6 +162,12 @@ export const getWideConfig = async (context: Context) => { openAITokenLimit: mergedConfigData.openAITokenLimit, staleBountyTime: mergedConfigData.staleBountyTime, newContributorGreeting: mergedConfigData.newContributorGreeting, + timeRangeForMaxIssue: mergedConfigData.timeRangeForMaxIssue, + timeRangeForMaxIssueEnabled: mergedConfigData.timeRangeForMaxIssueEnabled, + permitBaseUrl: mergedConfigData.permitBaseUrl, + botDelay: mergedConfigData.botDelay, + followUpTime: mergedConfigData.followUpTime, + disqualifyTime: mergedConfigData.disqualifyTime, }; return configData; From 9d00d7f8be6e7b2eeb91f8a01246a1d9697881d7 Mon Sep 17 00:00:00 2001 From: me505 <62057938+me505@users.noreply.github.com> Date: Tue, 3 Oct 2023 07:03:27 +0530 Subject: [PATCH 23/23] feat: price overide (#735) * feat: price override * feat: price override * feat: price overide * chore: t * chore: t * chore: t * feat: price override * feat: price override * feat: price override * feat: price override * feat: price override * feat: price override * feat: price override * feat: price override * feat: price override * refactor: simplify --------- Co-authored-by: 0xcodercrane <108444211+0xcodercrane@users.noreply.github.com> --- src/handlers/pricing/action.ts | 36 ++++++++++++++++++++---- src/handlers/wildcard/analytics.ts | 4 +-- src/helpers/issue.ts | 44 ++++++++++++++++++++++++++++++ 3 files changed, 77 insertions(+), 7 deletions(-) diff --git a/src/handlers/pricing/action.ts b/src/handlers/pricing/action.ts index 0b8982eaa..7e9673c6f 100644 --- a/src/handlers/pricing/action.ts +++ b/src/handlers/pricing/action.ts @@ -1,7 +1,7 @@ import { getBotConfig, getBotContext, getLogger } from "../../bindings"; import { GLOBAL_STRINGS } from "../../configs"; -import { addCommentToIssue, addLabelToIssue, clearAllPriceLabelsOnIssue, createLabel, getLabel, calculateWeight } from "../../helpers"; -import { Payload } from "../../types"; +import { addCommentToIssue, addLabelToIssue, clearAllPriceLabelsOnIssue, createLabel, getLabel, calculateWeight, getAllLabeledEvents } from "../../helpers"; +import { Payload, UserType } from "../../types"; import { handleLabelsAccess } from "../access"; import { getTargetPriceLabel } from "../shared"; @@ -12,7 +12,7 @@ export const pricingLabelLogic = async (): Promise => { const payload = context.payload as Payload; if (!payload.issue) return; const labels = payload.issue.labels; - + const labelNames = labels.map((i) => i.name); logger.info(`Checking if the issue is a parent issue.`); if (payload.issue.body && isParentIssue(payload.issue.body)) { logger.error("Identified as parent issue. Disabling price label."); @@ -37,10 +37,36 @@ export const pricingLabelLogic = async (): Promise => { const minPriorityLabel = priorityLabels.length > 0 ? priorityLabels.reduce((a, b) => (calculateWeight(a) < calculateWeight(b) ? a : b)).name : undefined; const targetPriceLabel = getTargetPriceLabel(minTimeLabel, minPriorityLabel); + if (targetPriceLabel) { - if (labels.map((i) => i.name).includes(targetPriceLabel)) { - logger.info(`Skipping... already exists`); + const _targetPriceLabel = labelNames.find((name) => name.includes("Price") && name.includes(targetPriceLabel)); + + if (_targetPriceLabel) { + // get all issue events of type "labeled" and the event label includes Price + let labeledEvents = await getAllLabeledEvents(); + if (!labeledEvents) return; + + labeledEvents = labeledEvents.filter((event) => event.label?.name.includes("Price")); + if (!labeledEvents.length) return; + + // check if the latest price label has been added by a user + if (labeledEvents[labeledEvents.length - 1].actor?.type == UserType.User) { + logger.info(`Skipping... already exists`); + } else { + // add price label to issue becuase wrong price has been added by bot + logger.info(`Adding price label to issue`); + await clearAllPriceLabelsOnIssue(); + + const exist = await getLabel(targetPriceLabel); + + if (assistivePricing && !exist) { + logger.info(`${targetPriceLabel} doesn't exist on the repo, creating...`); + await createLabel(targetPriceLabel, "price"); + } + await addLabelToIssue(targetPriceLabel); + } } else { + // add price if there is none logger.info(`Adding price label to issue`); await clearAllPriceLabelsOnIssue(); diff --git a/src/handlers/wildcard/analytics.ts b/src/handlers/wildcard/analytics.ts index 62f24edf9..df877f070 100644 --- a/src/handlers/wildcard/analytics.ts +++ b/src/handlers/wildcard/analytics.ts @@ -2,7 +2,6 @@ import { getMaxIssueNumber, upsertIssue, upsertUser } from "../../adapters/supab import { getBotConfig, getLogger } from "../../bindings"; import { listIssuesForRepo, getUser, calculateWeight } from "../../helpers"; import { Issue, IssueType, User, UserProfile } from "../../types"; -import { getTargetPriceLabel } from "../shared"; /** * Checks the issue whether it's a bounty for hunters or an issue for not @@ -19,6 +18,7 @@ export const bountyInfo = ( } => { const config = getBotConfig(); const labels = issue.labels; + const timeLabels = config.price.timeLabels.filter((item) => labels.map((i) => i.name).includes(item.name)); const priorityLabels = config.price.priorityLabels.filter((item) => labels.map((i) => i.name).includes(item.name)); @@ -27,7 +27,7 @@ export const bountyInfo = ( const minTimeLabel = timeLabels.length > 0 ? timeLabels.reduce((a, b) => (calculateWeight(a) < calculateWeight(b) ? a : b)).name : undefined; const minPriorityLabel = priorityLabels.length > 0 ? priorityLabels.reduce((a, b) => (calculateWeight(a) < calculateWeight(b) ? a : b)).name : undefined; - const priceLabel = getTargetPriceLabel(minTimeLabel, minPriorityLabel); + const priceLabel = labels.find((label) => label.name.includes("Price"))?.name; return { isBounty, diff --git a/src/helpers/issue.ts b/src/helpers/issue.ts index 27ab9562d..c3cc89972 100644 --- a/src/helpers/issue.ts +++ b/src/helpers/issue.ts @@ -3,6 +3,50 @@ import { getBotConfig, getBotContext, getLogger } from "../bindings"; import { AssignEvent, Comment, IssueType, Payload, StreamlinedComment, UserType } from "../types"; import { checkRateLimitGit } from "../utils"; +export const getAllIssueEvents = async () => { + const context = getBotContext(); + const logger = getLogger(); + const payload = context.payload as Payload; + if (!payload.issue) return; + + let shouldFetch = true; + let page_number = 1; + const events = []; + + try { + while (shouldFetch) { + // Fetch issue events + const response = await context.octokit.issues.listEvents({ + owner: payload.repository.owner.login, + repo: payload.repository.full_name, + issue_number: payload.issue.number, + per_page: 100, + page: page_number, + }); + + await checkRateLimitGit(response?.headers); + + if (response?.data?.length > 0) { + events.push(...response.data); + page_number++; + } else { + shouldFetch = false; + } + } + } catch (e: unknown) { + shouldFetch = false; + logger.error(`Getting all issue events failed, reason: ${e}`); + return null; + } + return events; +}; + +export const getAllLabeledEvents = async () => { + const events = await getAllIssueEvents(); + if (!events) return null; + return events.filter((event) => event.event === "labeled"); +}; + export const clearAllPriceLabelsOnIssue = async (): Promise => { const context = getBotContext(); const logger = getLogger();