Skip to content
This repository has been archived by the owner on Sep 19, 2024. It is now read-only.

feat: selectivly enable incentives #781

Closed
wants to merge 12 commits into from
90 changes: 47 additions & 43 deletions src/configs/ubiquibot-config-default.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MergedConfig } from "../types";

export const DefaultConfig : MergedConfig = {
export const DefaultConfig: MergedConfig = {
"evm-network-id": 100,
"price-multiplier": 1,
"issue-creator-multiplier": 2,
Expand All @@ -10,88 +10,92 @@ export const DefaultConfig : MergedConfig = {
"disable-analytics": false,
"comment-incentives": false,
"register-wallet-with-verification": false,
"promotion-comment": "\n<h6>If you enjoy the DevPool experience, please follow <a href='https://github.com/ubiquity'>Ubiquity on GitHub</a> and star <a href='https://github.com/ubiquity/devpool-directory'>this repo</a> to show your support. It helps a lot!</h6>",
"promotion-comment":
"\n<h6>If you enjoy the DevPool experience, please follow <a href='https://github.com/ubiquity'>Ubiquity on GitHub</a> and star <a href='https://github.com/ubiquity/devpool-directory'>this repo</a> to show your support. It helps a lot!</h6>",
"default-labels": [],
"time-labels": [
{
"name": "Time: <1 Hour"
name: "Time: <1 Hour",
},
{
"name": "Time: <1 Day"
name: "Time: <1 Day",
},
{
"name": "Time: <1 Week"
name: "Time: <1 Week",
},
{
"name": "Time: <2 Weeks"
name: "Time: <2 Weeks",
},
{
"name": "Time: <1 Month"
}
name: "Time: <1 Month",
},
],
"priority-labels": [
{
"name": "Priority: 1 (Normal)"
name: "Priority: 1 (Normal)",
},
{
"name": "Priority: 2 (Medium)"
name: "Priority: 2 (Medium)",
},
{
"name": "Priority: 3 (High)"
name: "Priority: 3 (High)",
},
{
"name": "Priority: 4 (Urgent)"
name: "Priority: 4 (Urgent)",
},
{
"name": "Priority: 5 (Emergency)"
}
name: "Priority: 5 (Emergency)",
},
],
"command-settings": [
{
"name": "start",
"enabled": false
name: "start",
enabled: false,
},
{
name: "stop",
enabled: false,
},
{
"name": "stop",
"enabled": false
name: "wallet",
enabled: false,
},
{
"name": "wallet",
"enabled": false
name: "payout",
enabled: false,
},
{
"name": "payout",
"enabled": false
name: "multiplier",
enabled: false,
},
{
"name": "multiplier",
"enabled": false
name: "query",
enabled: false,
},
{
"name": "query",
"enabled": false
name: "comment-incentive",
enabled: false,
},
{
"name": "allow",
"enabled": false
name: "allow",
enabled: false,
},
{
"name": "autopay",
"enabled": false
}
name: "autopay",
enabled: false,
},
],
"incentives": {
"comment": {
"elements": {},
"totals": {
"word": 0
}
}
incentives: {
comment: {
elements: {},
totals: {
word: 0,
},
},
},
"enable-access-control": {
"label": false,
"organization": true
label: false,
organization: true,
},
"stale-bounty-time":"0d"
}

"stale-bounty-time": "0d",
};
1 change: 1 addition & 0 deletions src/handlers/comment/commands.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export enum IssueCommentCommands {
PAYOUT = "/payout", // request permit payout
MULTIPLIER = "/multiplier", // set bounty multiplier (for treasury)
QUERY = "/query",
COMMENTINCENTIVE = "/comment-incentive",
// Access Controls

ALLOW = "/allow",
Expand Down
23 changes: 23 additions & 0 deletions src/handlers/comment/handlers/comment-incentives.ts
Original file line number Diff line number Diff line change
@@ -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") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

you can create a helper function that parses the command so you can use it here and in getIncentivizedUsers to avoid repeating code

return `invalid syntax for /comment-incentive \n usage /comment-incentive @user @user1... true|false \n ex /comment-incentive @user true`;
} else {
return;
}
};
8 changes: 8 additions & 0 deletions src/handlers/comment/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import { query } from "./query";
import { autoPay } from "./payout";
import { getTargetPriceLabel } from "../../shared";
import { ErrorDiff } from "../../../utils/helpers";
import { commentIncentive } from "./comment-incentives";

export * from "./assign";
export * from "./wallet";
Expand All @@ -36,6 +37,7 @@ export * from "./payout";
export * from "./help";
export * from "./multiplier";
export * from "./query";
export * from "./comment-incentives";

/**
* Parses the comment body and figure out the command name a user wants
Expand Down Expand Up @@ -245,6 +247,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`,
Expand Down
49 changes: 26 additions & 23 deletions src/handlers/payout/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
generatePermit2Signature,
getAllIssueComments,
getAllPullRequestReviews,
getIncentivizedUsers,
getIssueDescription,
getTokenSymbol,
parseComments,
Expand Down Expand Up @@ -70,13 +71,12 @@ export const incentivizeComments = async () => {
logger.info("incentivizeComments: skipping payment permit generation because `assignee` is `undefined`.");
return;
}

const issueComments = await getAllIssueComments(issue.number, "full");
logger.info(`Getting the issue comments done. comments: ${JSON.stringify(issueComments)}`);
const issueCommentsByUser: Record<string, { id: string; comments: string[] }> = {};
for (const issueComment of issueComments) {
const user = issueComment.user;
if (user.type == UserType.Bot || user.login == assignee) 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)}`);
Expand All @@ -98,31 +98,34 @@ export const incentivizeComments = async () => {

// The mapping between gh handle and comment with a permit url
const reward: Record<string, string> = {};

const users = await getIncentivizedUsers(issue.number);
if (!users) return;
// The mapping between gh handle and amount in ETH
const fallbackReward: Record<string, Decimal> = {};
let comment = `#### Conversation Rewards\n`;
for (const user of Object.keys(issueCommentsByUser)) {
const commentsByUser = issueCommentsByUser[user];
const commentsByNode = await parseComments(commentsByUser.comments, ItemsToExclude);
const rewardValue = calculateRewardValue(commentsByNode, incentives);
if (rewardValue.equals(0)) {
logger.info(`Skipping to generate a permit url because the reward value is 0. user: ${user}`);
continue;
}
logger.debug(`Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`);
const account = await getWalletAddress(user);
const amountInETH = rewardValue.mul(baseMultiplier);
if (amountInETH.gt(paymentPermitMaxPrice)) {
logger.info(`Skipping comment reward for user ${user} because reward is higher than payment permit max price`);
continue;
}
if (account) {
const { payoutUrl } = await generatePermit2Signature(account, amountInETH, issue.node_id, commentsByUser.id, "ISSUE_COMMENTER");
comment = `${comment}### [ **${user}: [ CLAIM ${amountInETH} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n`;
reward[user] = payoutUrl;
} else {
fallbackReward[user] = amountInETH;
if (users[user] || users[user] == true) {
const commentsByUser = issueCommentsByUser[user];
const commentsByNode = await parseComments(commentsByUser.comments, ItemsToExclude);
const rewardValue = calculateRewardValue(commentsByNode, incentives);
if (rewardValue.equals(0)) {
logger.info(`Skipping to generate a permit url because the reward value is 0. user: ${user}`);
continue;
}
logger.debug(`Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`);
const account = await getWalletAddress(user);
const amountInETH = rewardValue.mul(baseMultiplier);
if (amountInETH.gt(paymentPermitMaxPrice)) {
logger.info(`Skipping comment reward for user ${user} because reward is higher than payment permit max price`);
continue;
}
if (account) {
const { payoutUrl } = await generatePermit2Signature(account, amountInETH, issue.node_id, commentsByUser.id, "ISSUE_COMMENTER");
comment = `${comment}### [ **${user}: [ CLAIM ${amountInETH} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n`;
reward[user] = payoutUrl;
} else {
fallbackReward[user] = amountInETH;
}
}
}

Expand Down
26 changes: 26 additions & 0 deletions src/helpers/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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") {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this relies on command order. just use a for loop and check for true, false, or starts with @ (there shouldn't more than one true or false). you can look at this example

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();
Expand Down