diff --git a/src/configs/ubiquibot-config-default.ts b/src/configs/ubiquibot-config-default.ts index 4a989995f..465728c54 100644 --- a/src/configs/ubiquibot-config-default.ts +++ b/src/configs/ubiquibot-config-default.ts @@ -48,6 +48,10 @@ export const DefaultConfig: MergedConfig = { }, ], commandSettings: [ + { + name: "start", + enabled: false, + }, { name: "start", enabled: false, @@ -72,6 +76,10 @@ export const DefaultConfig: MergedConfig = { name: "query", enabled: false, }, + { + name: "comment-incentive", + enabled: false, + }, { name: "ask", enabled: false, diff --git a/src/handlers/comment/commands.ts b/src/handlers/comment/commands.ts index 9bce9fc6e..1e390e581 100644 --- a/src/handlers/comment/commands.ts +++ b/src/handlers/comment/commands.ts @@ -6,6 +6,7 @@ export enum IssueCommentCommands { PAYOUT = "/payout", // request permit payout MULTIPLIER = "/multiplier", // set bounty multiplier (for treasury) QUERY = "/query", + COMMENTINCENTIVE = "/comment-incentive", ASK = "/ask", // ask GPT a question // Access Controls diff --git a/src/handlers/comment/handlers/comment-incentives.ts b/src/handlers/comment/handlers/comment-incentives.ts new file mode 100644 index 000000000..5a2cf6496 --- /dev/null +++ b/src/handlers/comment/handlers/comment-incentives.ts @@ -0,0 +1,23 @@ +import { getBotContext, getLogger } from "../../../bindings"; +import { Payload } from "../../../types"; + +export const commentIncentive = async (body: string) => { + const context = getBotContext(); + const logger = getLogger(); + const payload = context.payload as Payload; + const sender = payload.sender.login; + + logger.info(`Received '/comment-incentive' command from user: ${sender}`); + + if (!payload.issue) { + logger.info(`Skipping '/comment-incentive' because of no issue instance`); + return `Skipping '/comment-incentive' because of no issue instance`; + } + const parts = body.split(" "); + parts.shift(); + if (parts.pop() !== "true" && parts.pop() !== "false") { + return `invalid syntax for /comment-incentive \n usage /comment-incentive @user @user1... true|false \n ex /comment-incentive @user true`; + } else { + return; + } +}; diff --git a/src/handlers/comment/handlers/index.ts b/src/handlers/comment/handlers/index.ts index d7de92373..195ca1823 100644 --- a/src/handlers/comment/handlers/index.ts +++ b/src/handlers/comment/handlers/index.ts @@ -38,6 +38,7 @@ import { autoPay } from "./payout"; import { getTargetPriceLabel } from "../../shared"; import Decimal from "decimal.js"; import { ErrorDiff } from "../../../utils/helpers"; +import { commentIncentive } from "./comment-incentives"; export * from "./assign"; export * from "./wallet"; @@ -46,6 +47,7 @@ export * from "./payout"; export * from "./help"; export * from "./multiplier"; export * from "./query"; +export * from "./comment-incentives"; export * from "./ask"; export * from "./authorize"; @@ -282,6 +284,12 @@ export const userCommands = (): UserCommands[] => { handler: autoPay, callback: commandCallback, }, + { + id: IssueCommentCommands.COMMENTINCENTIVE, + description: `Enabled or disables comment incentives for a user`, + handler: commentIncentive, + callback: commandCallback, + }, { id: IssueCommentCommands.QUERY, description: `Comments the users multiplier and address`, diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index 18d9aa1df..81088341e 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -1,6 +1,12 @@ import { getWalletAddress } from "../../adapters/supabase"; import { getBotContext, getLogger } from "../../bindings"; -import { getAllIssueComments, getAllPullRequestReviews, getIssueDescription, parseComments } from "../../helpers"; +import { + getAllIssueComments, + getAllPullRequestReviews, + getIncentivizedUsers, + getIssueDescription, + parseComments, +} from "../../helpers"; import { getLatestPullRequest, gitLinkedPrParser } from "../../helpers/parser"; import { Incentives, MarkdownItem, Payload, UserType } from "../../types"; import { RewardsResponse, commentParser } from "../comment"; @@ -46,13 +52,16 @@ export const calculateIssueConversationReward = async (calculateIncentives: Ince logger.info("incentivizeComments: skipping payment permit generation because `assignee` is `undefined`."); return { error: "incentivizeComments: skipping payment permit generation because `assignee` is `undefined`." }; } - + const issueComments = await getAllIssueComments(calculateIncentives.issue.number, "full"); + logger.info(`Getting the issue comments done. comments: ${JSON.stringify(issueComments)}`); const issueCommentsByUser: Record = {}; for (const issueComment of issueComments) { const user = issueComment.user; - if (user.type == UserType.Bot || user.login == assignee.login) continue; + + if (user.type == UserType.Bot) continue; + const commands = commentParser(issueComment.body); if (commands.length > 0) { logger.info(`Skipping to parse the comment because it contains commands. comment: ${JSON.stringify(issueComment)}`); @@ -71,6 +80,11 @@ export const calculateIssueConversationReward = async (calculateIncentives: Ince } logger.info(`Filtering by the user type done. commentsByUser: ${JSON.stringify(issueCommentsByUser)}`); + // The mapping between gh handle and comment with a permit url + const reward: Record = {}; + const users = await getIncentivizedUsers(calculateIncentives.issue.number); + if (!users) return; + // The mapping between gh handle and amount in ETH const fallbackReward: Record = {}; @@ -78,24 +92,26 @@ export const calculateIssueConversationReward = async (calculateIncentives: Ince const reward: { account: string; priceInEth: Decimal; userId: string; user: string; penaltyAmount: BigNumber }[] = []; 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)) { - 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); - 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) }); - } else { - fallbackReward[user] = priceInEth; + if (users[user] || users[user] == true) { + const commentsByUser = issueCommentsByUser[user]; + const commentsByNode = await parseComments(commentsByUser.comments, ItemsToExclude); + const rewardValue = calculateRewardValue(commentsByNode, calculateIncentives.incentives); + if (rewardValue.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); + 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) }); + } else { + fallbackReward[user] = priceInEth; + } } } diff --git a/src/helpers/issue.ts b/src/helpers/issue.ts index e014994fe..df219d020 100644 --- a/src/helpers/issue.ts +++ b/src/helpers/issue.ts @@ -95,6 +95,32 @@ export const listAllIssuesForRepo = async (state: "open" | "closed" | "all" = "o return issuesArr; }; +export const getIncentivizedUsers = async (issue_number: number) => { + const comments = await getAllIssueComments(issue_number); + const incentiveComments = comments.filter((comment) => comment.body.startsWith("/comment-incentives")); + let users: { [key: string]: boolean } = {}; + for (const incentiveComment of incentiveComments) { + const parts = incentiveComment.body.split(" "); + parts.shift(); + if (parts.pop() == "true") { + for (const part of parts) { + if (part.startsWith("@")) { + users[part.substring(1)] = true; + } + } + } else if (parts.pop() == "false") { + for (const part of parts) { + if (part.startsWith("@")) { + users[part.substring(1)] = true; + } + } + } else { + return undefined; + } + } + return users; +}; + export const addCommentToIssue = async (msg: string, issue_number: number) => { const context = getBotContext(); const logger = getLogger();