Skip to content

Commit

Permalink
Merge branch 'development' into development-perf-1
Browse files Browse the repository at this point in the history
  • Loading branch information
Steveantor authored Sep 15, 2023
2 parents 1a58606 + 0154b7e commit 4a7c568
Show file tree
Hide file tree
Showing 9 changed files with 223 additions and 32 deletions.
15 changes: 12 additions & 3 deletions .github/ISSUE_TEMPLATE/bounty-template.yml
Original file line number Diff line number Diff line change
@@ -1,18 +1,20 @@
name: "Bounty Proposal"
description: Have a suggestion for how to improve UbiquiBot? Let us know!
title: "Bounty Proposal:"
title: "Bounty Proposal: "

body:
- type: markdown
attributes:
value: |
## Feature Request Form
Thank you for taking the time to file a feature request! Please let us know what you're trying to do, and how UbiquiBot can help.
Thank you for taking the time to file a feature request.
If you register your wallet address, you will be eligible for compensation if this is accepted!
Please let us know how we can improve the bot.
- type: textarea
attributes:
label: Describe the background or context
description: Please let us know what inspired you to write this proposal. Backlinking to specific comments is usually sufficient context.
description: Please let us know what inspired you to write this proposal. Backlinking to specific comments on GitHub, and leaving a remark about how the bot should have interacted with it is usually sufficient context.
validations:
required: false

Expand All @@ -22,3 +24,10 @@ body:
description: A clear description of what you want to happen. Add any considered drawbacks.
validations:
required: true

- type: textarea
attributes:
label: Remarks
description: Any closing remarks?
validations:
required: false
6 changes: 3 additions & 3 deletions .github/ISSUE_TEMPLATE/config.yml
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
blank_issues_enabled: true
contact_links:
- name: Telegram Group Chat
url: https://t.me/UbiquityDAO/29891
about: "Join us on Telegram!"
- name: UbiquiBot Development Group Chat
url: https://t.me/UbiquityDAO/31132
about: "Live chat with us on Telegram!"
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
"@sinclair/typebox": "^0.31.5",
"@supabase/supabase-js": "^2.4.0",
"@types/ms": "^0.7.31",
"@types/parse5": "^7.0.0",
"@typescript-eslint/eslint-plugin": "^5.59.11",
"@typescript-eslint/parser": "^5.59.11",
"@uniswap/permit2-sdk": "^1.2.0",
Expand Down
8 changes: 4 additions & 4 deletions src/handlers/assign/auto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,14 +18,14 @@ export const checkPullRequests = async () => {

// Loop through the pull requests and assign them to their respective issues if needed
for (const pull of pulls) {
const pullRequestLinked = await gitLinkedIssueParser({
const linkedIssue = await gitLinkedIssueParser({
owner: payload.repository.owner.login,
repo: payload.repository.name,
issue_number: pull.number,
pull_number: pull.number,
});

// if pullRequestLinked is empty, continue
if (pullRequestLinked == "" || !pull.user) {
if (linkedIssue == "" || !pull.user || !linkedIssue) {
continue;
}

Expand All @@ -37,7 +37,7 @@ export const checkPullRequests = async () => {
continue;
}

const linkedIssueNumber = pullRequestLinked.substring(pullRequestLinked.lastIndexOf("/") + 1);
const linkedIssueNumber = linkedIssue.substring(linkedIssue.lastIndexOf("/") + 1);

// Check if the pull request opener is assigned to the issue
const opener = pull.user.login;
Expand Down
131 changes: 131 additions & 0 deletions src/handlers/payout/post.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,13 @@ import {
generatePermit2Signature,
getAllIssueComments,
getIssueDescription,
getAllPullRequestReviews,
getOrgMembershipOfUser,
getTokenSymbol,
parseComments,
} from "../../helpers";
import { gitLinkedPrParser } from "../../helpers/parser";

import { Incentives, MarkdownItem, Payload, StateReason, UserType } from "../../types";
import { commentParser } from "../comment";
import Decimal from "decimal.js";
Expand Down Expand Up @@ -150,6 +153,134 @@ export const incentivizeComments = async () => {
await addCommentToIssue(comment, issue.number);
};

export const incentivizePullRequestReviews = async () => {
const logger = getLogger();
const {
mode: { incentiveMode, paymentPermitMaxPrice },
price: { baseMultiplier, incentives },
payout: { paymentToken, rpc },
} = getBotConfig();
if (!incentiveMode) {
logger.info(`No incentive mode. skipping to process`);
return;
}
const context = getBotContext();
const payload = context.payload as Payload;
const issue = payload.issue;
if (!issue) {
logger.info(`Incomplete payload. issue: ${issue}`);
return;
}

if (issue.state_reason !== StateReason.COMPLETED) {
logger.info("incentivizePullRequestReviews: comment incentives skipped because the issue was not closed as completed");
return;
}

if (paymentPermitMaxPrice == 0 || !paymentPermitMaxPrice) {
logger.info(`incentivizePullRequestReviews: skipping to generate permit2 url, reason: { paymentPermitMaxPrice: ${paymentPermitMaxPrice}}`);
return;
}

const issueDetailed = bountyInfo(issue);
if (!issueDetailed.isBounty) {
logger.info(`incentivizePullRequestReviews: its not a bounty`);
return;
}

const linkedPullRequest = await gitLinkedPrParser({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: issue.number });

if (!linkedPullRequest) {
logger.debug(`incentivizePullRequestReviews: No linked pull requests found`);
return;
}

const comments = await getAllIssueComments(issue.number);
const permitComments = comments.filter(
(content) => content.body.includes("Reviewer Rewards") && content.body.includes("https://pay.ubq.fi?claim=") && content.user.type == UserType.Bot
);
if (permitComments.length > 0) {
logger.info(`incentivizePullRequestReviews: skip to generate a permit url because it has been already posted`);
return;
}

const assignees = issue?.assignees ?? [];
const assignee = assignees.length > 0 ? assignees[0] : undefined;
if (!assignee) {
logger.info("incentivizePullRequestReviews: skipping payment permit generation because `assignee` is `undefined`.");
return;
}

const prReviews = await getAllPullRequestReviews(context, linkedPullRequest.number, "full");
const prComments = await getAllIssueComments(linkedPullRequest.number, "full");
logger.info(`Getting the PR reviews done. comments: ${JSON.stringify(prReviews)}`);
const prReviewsByUser: Record<string, { id: string; comments: string[] }> = {};
for (const review of prReviews) {
const user = review.user;
if (!user) continue;
if (user.type == UserType.Bot || user.login == assignee) continue;
if (!review.body_html) {
logger.info(`incentivizePullRequestReviews: Skipping to parse the comment because body_html is undefined. comment: ${JSON.stringify(review)}`);
continue;
}
if (!prReviewsByUser[user.login]) {
prReviewsByUser[user.login] = { id: user.node_id, comments: [] };
}
prReviewsByUser[user.login].comments.push(review.body_html);
}

for (const comment of prComments) {
const user = comment.user;
if (!user) continue;
if (user.type == UserType.Bot || user.login == assignee) continue;
if (!comment.body_html) {
logger.info(`incentivizePullRequestReviews: Skipping to parse the comment because body_html is undefined. comment: ${JSON.stringify(comment)}`);
continue;
}
if (!prReviewsByUser[user.login]) {
prReviewsByUser[user.login] = { id: user.node_id, comments: [] };
}
prReviewsByUser[user.login].comments.push(comment.body_html);
}
const tokenSymbol = await getTokenSymbol(paymentToken, rpc);
logger.info(`incentivizePullRequestReviews: Filtering by the user type done. commentsByUser: ${JSON.stringify(prReviewsByUser)}`);

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

// The mapping between gh handle and amount in ETH
const fallbackReward: Record<string, Decimal> = {};
let comment = `#### Reviewer Rewards\n`;
for (const user of Object.keys(prReviewsByUser)) {
const commentByUser = prReviewsByUser[user];
const commentsByNode = await parseComments(commentByUser.comments, ItemsToExclude);
const rewardValue = calculateRewardValue(commentsByNode, incentives);
if (rewardValue.equals(0)) {
logger.info(`incentivizePullRequestReviews: Skipping to generate a permit url because the reward value is 0. user: ${user}`);
continue;
}
logger.info(`incentivizePullRequestReviews: Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`);
const account = await getWalletAddress(user);
const amountInETH = rewardValue.mul(baseMultiplier);
if (amountInETH.gt(paymentPermitMaxPrice)) {
logger.info(`incentivizePullRequestReviews: 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, commentByUser.id, "ISSUE_COMMENTER");
comment = `${comment}### [ **${user}: [ CLAIM ${amountInETH} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n`;
reward[user] = payoutUrl;
} else {
fallbackReward[user] = amountInETH;
}
}

logger.info(`incentivizePullRequestReviews: Permit url generated for pull request reviewers. reward: ${JSON.stringify(reward)}`);
logger.info(`incentivizePullRequestReviews: Skipping to generate a permit url for missing accounts. fallback: ${JSON.stringify(fallbackReward)}`);

await addCommentToIssue(comment, issue.number);
};

export const incentivizeCreatorComment = async () => {
const logger = getLogger();
const {
Expand Down
4 changes: 2 additions & 2 deletions src/handlers/processors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,8 @@ import { nullHandler } from "./shared";
import { handleComment, issueClosedCallback, issueCreatedCallback, issueReopenedCallback } from "./comment";
import { checkPullRequests } from "./assign/auto";
import { createDevPoolPR } from "./pull-request";
import { incentivizeComments, incentivizeCreatorComment, incentivizePullRequestReviews } from "./payout";
import { runOnPush, validateConfigChange } from "./push";
import { incentivizeComments, incentivizeCreatorComment } from "./payout";
import { findDuplicateOne } from "./issue";

export const processors: Record<string, Handler> = {
Expand Down Expand Up @@ -54,7 +54,7 @@ export const processors: Record<string, Handler> = {
[GithubEvent.ISSUES_CLOSED]: {
pre: [nullHandler],
action: [issueClosedCallback],
post: [incentivizeCreatorComment, incentivizeComments],
post: [incentivizeCreatorComment, incentivizeComments, incentivizePullRequestReviews],
},
[GithubEvent.PULL_REQUEST_OPENED]: {
pre: [nullHandler],
Expand Down
15 changes: 12 additions & 3 deletions src/helpers/issue.ts
Original file line number Diff line number Diff line change
Expand Up @@ -515,13 +515,13 @@ export const closePullRequest = async (pull_number: number) => {
}
};

export const getAllPullRequestReviews = async (context: Context, pull_number: number) => {
export const getAllPullRequestReviews = async (context: Context, pull_number: number, format: "raw" | "html" | "text" | "full" = "raw") => {
const prArr = [];
let fetchDone = false;
const perPage = 30;
let curPage = 1;
while (!fetchDone) {
const prs = await getPullRequestReviews(context, pull_number, perPage, curPage);
const prs = await getPullRequestReviews(context, pull_number, perPage, curPage, format);

// push the objects to array
prArr.push(...prs);
Expand All @@ -532,7 +532,13 @@ export const getAllPullRequestReviews = async (context: Context, pull_number: nu
return prArr;
};

export const getPullRequestReviews = async (context: Context, pull_number: number, per_page: number, page: number) => {
export const getPullRequestReviews = async (
context: Context,
pull_number: number,
per_page: number,
page: number,
format: "raw" | "html" | "text" | "full" = "raw"
) => {
const logger = getLogger();
const payload = context.payload as Payload;
try {
Expand All @@ -542,6 +548,9 @@ export const getPullRequestReviews = async (context: Context, pull_number: numbe
pull_number,
per_page,
page,
mediaType: {
format,
},
});
return reviews;
} catch (e: unknown) {
Expand Down
60 changes: 51 additions & 9 deletions src/helpers/parser.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import axios from "axios";
import { HTMLElement, parse } from "node-html-parser";
import { getPullByNumber } from "./issue";
import { getBotContext, getLogger } from "../bindings";
import { Payload } from "../types";

interface GitParser {
owner: string;
repo: string;
issue_number: number;
issue_number?: number;
pull_number?: number;
}

export const gitIssueParser = async ({ owner, repo, issue_number }: GitParser): Promise<boolean> => {
Expand All @@ -25,20 +29,58 @@ export const gitIssueParser = async ({ owner, repo, issue_number }: GitParser):
}
};

export const gitLinkedIssueParser = async ({ owner, repo, issue_number }: GitParser): Promise<string> => {
export const gitLinkedIssueParser = async ({ owner, repo, pull_number }: GitParser) => {
const logger = getLogger();
try {
const { data } = await axios.get(`https://github.com/${owner}/${repo}/pull/${issue_number}`);
const { data } = await axios.get(`https://github.com/${owner}/${repo}/pull/${pull_number}`);
const dom = parse(data);
const devForm = dom.querySelector("[data-target='create-branch.developmentForm']") as HTMLElement;
const linkedPRs = devForm.querySelectorAll(".my-1");
const linkedIssues = devForm.querySelectorAll(".my-1");

if (linkedPRs.length === 0) {
return "";
if (linkedIssues.length === 0) {
return null;
}

const prUrl = linkedPRs[0].querySelector("a")?.attrs?.href || "";
return prUrl;
const issueUrl = linkedIssues[0].querySelector("a")?.attrs?.href || "";
return issueUrl;
} catch (error) {
logger.error(`${JSON.stringify(error)}`);
return null;
}
};

export const gitLinkedPrParser = async ({ owner, repo, issue_number }: GitParser) => {
const logger = getLogger();
try {
const { data } = await axios.get(`https://github.com/${owner}/${repo}/issues/${issue_number}`);
const context = getBotContext();
const payload = context.payload as Payload;
const dom = parse(data);
const devForm = dom.querySelector("[data-target='create-branch.developmentForm']") as HTMLElement;
const linkedPRs = devForm.querySelectorAll(".my-1");
if (linkedPRs.length === 0) return null;
let linkedPullRequest = null;
for (const linkedPr of linkedPRs) {
const prHref = linkedPr.querySelector("a")?.attrs?.href || "";
const parts = prHref.split("/");
// extract the organization name and repo name from the link:(e.g. "https://github.com/wannacfuture/ubiquibot/pull/5";)
const organization = parts[parts.length - 4];
const repository = parts[parts.length - 3];

if (`${organization}/${repository}` !== payload.repository.full_name) continue;
const prNumber = parts[parts.length - 1];
if (Number.isNaN(Number(prNumber))) return null;
const pr = await getPullByNumber(context, Number(prNumber));
if (!pr || !pr.merged) continue;

if (!linkedPullRequest) linkedPullRequest = pr;
else if (linkedPullRequest.merged_at && pr.merged_at && new Date(linkedPullRequest.merged_at) < new Date(pr.merged_at)) {
linkedPullRequest = pr;
}
}
return linkedPullRequest;
} catch (error) {
return "";
logger.error(`${JSON.stringify(error)}`);
return null;
}
};
15 changes: 7 additions & 8 deletions src/helpers/shared.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import ms from "ms";
import { getBotContext } from "../bindings";
import { LabelItem, Payload, UserType } from "../types";

Expand Down Expand Up @@ -35,13 +36,11 @@ export const calculateWeight = (label: LabelItem | undefined): number => {

export const calculateDuration = (label: LabelItem): number => {
if (!label) return 0;
const matches = label.name.match(/\d+/);
if (label.name.toLowerCase().includes("priority")) return 0;
const number = matches && matches.length > 0 ? parseInt(matches[0]) || 0 : 0;
if (label.name.toLowerCase().includes("minute")) return number * 60;
if (label.name.toLowerCase().includes("hour")) return number * 3600;
if (label.name.toLowerCase().includes("day")) return number * 86400;
if (label.name.toLowerCase().includes("week")) return number * 604800;
if (label.name.toLowerCase().includes("month")) return number * 2592000;
return 0;

const pattern = /<(\d+\s\w+)/;
const result = label.name.match(pattern);
if (!result) return 0;

return ms(result[1]) / 1000;
};

0 comments on commit 4a7c568

Please sign in to comment.