diff --git a/src/configs/ubiquibot-config-default.ts b/src/configs/ubiquibot-config-default.ts index 9598ef782..93334d2df 100644 --- a/src/configs/ubiquibot-config-default.ts +++ b/src/configs/ubiquibot-config-default.ts @@ -52,6 +52,10 @@ export const DefaultConfig: MergedConfig = { name: "start", enabled: false, }, + { + name: "comment-incentive", + enabled: false, + }, { name: "stop", enabled: false, diff --git a/src/handlers/comment/commands.ts b/src/handlers/comment/commands.ts index 9bce9fc6e..7c6f4eff3 100644 --- a/src/handlers/comment/commands.ts +++ b/src/handlers/comment/commands.ts @@ -7,6 +7,7 @@ export enum IssueCommentCommands { MULTIPLIER = "/multiplier", // set bounty multiplier (for treasury) QUERY = "/query", ASK = "/ask", // ask GPT a question + COMMENTINCENTIVE = "/comment-incentive", // Access Controls ALLOW = "/allow", diff --git a/src/handlers/comment/handlers/comment-incentive.ts b/src/handlers/comment/handlers/comment-incentive.ts new file mode 100644 index 000000000..8483276c6 --- /dev/null +++ b/src/handlers/comment/handlers/comment-incentive.ts @@ -0,0 +1,22 @@ +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.debug(`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 toggle = (body.includes("true") || body.includes("false")) && body.match(/@([\w-]+)/g); + if (!toggle) { + 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 4dcbf7f21..09b081754 100644 --- a/src/handlers/comment/handlers/index.ts +++ b/src/handlers/comment/handlers/index.ts @@ -10,6 +10,7 @@ import { approveLabelChange } from "./authorize"; import { setAccess } from "./allow"; import { ask } from "./ask"; import { multiplier } from "./multiplier"; + import { BigNumber, ethers } from "ethers"; import { addPenalty } from "../../../adapters/supabase"; import { @@ -33,6 +34,7 @@ import { calculateIssueAssigneeReward, calculatePullRequestReviewsReward, } from "../../payout"; +import { commentIncentive } from "./comment-incentive"; import { query } from "./query"; import { autoPay } from "./payout"; import { getTargetPriceLabel } from "../../shared"; @@ -48,6 +50,7 @@ export * from "./multiplier"; export * from "./query"; export * from "./ask"; export * from "./authorize"; +export * from "./comment-incentive"; export interface RewardsResponse { error: string | null; @@ -74,7 +77,7 @@ export interface RewardsResponse { */ export const commentParser = (body: string): IssueCommentCommands[] => { - const regex = /^\/(\w+)\b/; // Regex pattern to match the command at the beginning of the body + const regex = /^\/([\w-]+)\b/; // Regex pattern to match the command at the beginning of the body const matches = regex.exec(body); if (matches) { @@ -264,6 +267,12 @@ export const userCommands = (): UserCommands[] => { handler: unassign, callback: commandCallback, }, + { + id: IssueCommentCommands.COMMENTINCENTIVE, + description: "Enables or Disables comment incentive for a user", + handler: commentIncentive, + callback: commandCallback, + }, { handler: listAvailableCommands, id: IssueCommentCommands.HELP, diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index e1ef60e76..fbf88427b 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -1,6 +1,6 @@ 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"; @@ -44,7 +44,7 @@ export const calculateIssueConversationReward = async (calculateIncentives: Ince 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)}`); @@ -75,8 +75,11 @@ export const calculateIssueConversationReward = async (calculateIncentives: Ince penaltyAmount: BigNumber; debug: Record; }[] = []; + const users = await getIncentivizedUsers(calculateIncentives.issue.number); + if (!users) return { error: "Error: Could not find any incentivized users" }; for (const user of Object.keys(issueCommentsByUser)) { + if (!users[user]) continue; const commentsByUser = issueCommentsByUser[user]; const commentsByNode = await parseComments(commentsByUser.comments, ItemsToExclude); const rewardValue = calculateRewardValue(commentsByNode, calculateIncentives.incentives); diff --git a/src/helpers/issue.ts b/src/helpers/issue.ts index 8cd78b047..f88d58d3a 100644 --- a/src/helpers/issue.ts +++ b/src/helpers/issue.ts @@ -71,6 +71,28 @@ export const clearAllPriceLabelsOnIssue = async (): Promise => { } }; +export const getIncentivizedUsers = async (issue_number: number) => { + const comments = await getAllIssueComments(issue_number); + const incentiveComments = comments.filter((comment) => comment.body.startsWith("/comment-incentive")); + const users: { [key: string]: boolean } = {}; + for (const incentiveComment of incentiveComments) { + const parts = incentiveComment.body.split(" "); + parts.shift(); + const toggle: RegExpMatchArray | null = incentiveComment.body.match(/\b(true|false)\b/); + + if (!toggle) { + continue; + } else { + for (const part of parts) { + if (part.startsWith("@")) { + users[part.substring(1)] = toggle[0] === "true"; + } + } + } + } + return users; +}; + export const addLabelToIssue = async (labelName: string) => { const context = getBotContext(); const logger = getLogger();