Skip to content

Commit

Permalink
Merge branch 'development' into chore/descriptions
Browse files Browse the repository at this point in the history
# Conflicts:
#	dist/index.js
  • Loading branch information
gentlementlegen committed Dec 5, 2024
2 parents f9b197d + fc5f479 commit 14bfd78
Show file tree
Hide file tree
Showing 12 changed files with 223 additions and 20 deletions.
3 changes: 2 additions & 1 deletion .cspell.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,8 @@
"mswjs",
"Rpcs",
"sonarjs",
"pico"
"pico",
"timespan"
],
"dictionaries": ["typescript", "node", "software-terms"],
"import": [
Expand Down
1 change: 1 addition & 0 deletions .gitattributes
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
dist/** linguist-generated
13 changes: 5 additions & 8 deletions .github/workflows/formatting-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,11 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: "20.10.0"
- uses: oven-sh/setup-bun@v2

- name: Run formatting checks
run: |
yarn install
yarn eslint --fix-dry-run --ignore-pattern dist/
yarn format:cspell
yarn prettier --check .
bun install
bun eslint --fix-dry-run --ignore-pattern dist/
bun format:cspell
bun prettier --check .
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,8 @@ with:
relevance: 1
userExtractor:
redeemTask: true
dataPurge: {}
dataPurge:
skipCommentsWhileAssigned: all
formattingEvaluator:
wordCountExponent: 0.85
multipliers:
Expand Down
Binary file modified bun.lockb
Binary file not shown.
22 changes: 21 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -273,7 +273,27 @@
"anyOf": [
{
"type": "object",
"properties": {}
"properties": {
"skipCommentsWhileAssigned": {
"default": "all",
"description": "Configures how user comments are included in the rewards calculation when they are assigned to a GitHub issue:\n\n- 'all': Excludes all comments made between the first assignment start and the last assignment end, discouraging gaming by un-assigning and commenting for rewards.\n- 'exact': Excludes only comments made during precise assignment periods, targeting times when the user is actively assigned.\n- 'none': Includes all comments, regardless of assignment status or timing.",
"examples": ["all", "exact", "none"],
"anyOf": [
{
"const": "all",
"type": "string"
},
{
"const": "exact",
"type": "string"
},
{
"const": "none",
"type": "string"
}
]
}
}
},
{
"type": "null"
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@
"@supabase/supabase-js": "2.42.0",
"@ubiquity-dao/rpc-handler": "1.3.0",
"@ubiquity-os/permit-generation": "^2.0.6",
"@ubiquity-os/plugin-sdk": "^1.1.0",
"@ubiquity-os/plugin-sdk": "^1.1.1",
"@ubiquity-os/ubiquity-os-logger": "^1.3.2",
"decimal.js": "10.4.3",
"ethers": "^5.7.2",
Expand Down
12 changes: 11 additions & 1 deletion src/configuration/data-purge-config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import { Type, Static } from "@sinclair/typebox";

export const dataPurgeConfigurationType = Type.Object({});
export const dataPurgeConfigurationType = Type.Object({
skipCommentsWhileAssigned: Type.Union([Type.Literal("all"), Type.Literal("exact"), Type.Literal("none")], {
default: "all",
description:
"Configures how user comments are included in the rewards calculation when they are assigned to a GitHub issue:\n\n" +
"- 'all': Excludes all comments made between the first assignment start and the last assignment end, discouraging gaming by un-assigning and commenting for rewards.\n" +
"- 'exact': Excludes only comments made during precise assignment periods, targeting times when the user is actively assigned.\n" +
"- 'none': Includes all comments, regardless of assignment status or timing.",
examples: ["all", "exact", "none"],
}),
});

export type DataPurgeConfiguration = Static<typeof dataPurgeConfigurationType>;
77 changes: 77 additions & 0 deletions src/helpers/user-assigned-timespan.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import { IssueParams } from "../start";
import { ContextPlugin } from "../types/plugin-input";
import { IssueActivity } from "../issue-activity";

export interface AssignmentPeriod {
assignedAt: string;
unassignedAt: string | null;
}

export interface UserAssignments {
[username: string]: AssignmentPeriod[];
}

/*
* Returns the list of assignment periods per user for a given issue.
*/
export async function getAssignmentPeriods(octokit: ContextPlugin["octokit"], issueParams: IssueParams) {
const events = await octokit.paginate(octokit.rest.issues.listEvents, {
...issueParams,
per_page: 100,
});

const userAssignments: UserAssignments = {};

events
.filter((event) => ["assigned", "unassigned"].includes(event.event))
.forEach((event) => {
const username = "assignee" in event ? event.assignee?.login : null;
if (!username) return;

if (!userAssignments[username]) {
userAssignments[username] = [];
}

const lastPeriod = userAssignments[username][userAssignments[username].length - 1];

if (event.event === "assigned") {
userAssignments[username].push({
assignedAt: event.created_at,
unassignedAt: null,
});
} else if (event.event === "unassigned" && lastPeriod && lastPeriod.unassignedAt === null) {
lastPeriod.unassignedAt = event.created_at;
}
});

Object.values(userAssignments).forEach((periods) => {
const lastPeriod = periods[periods.length - 1];
if (lastPeriod && lastPeriod.unassignedAt === null) {
lastPeriod.unassignedAt = new Date().toISOString();
}
});

return userAssignments;
}

export function isCommentDuringAssignment(
comment: IssueActivity["allComments"][0],
assignments: AssignmentPeriod[],
isExact: boolean
) {
const commentDate = new Date(comment.created_at);
if (!assignments?.length) {
return false;
}
if (!isExact) {
const assignedAt = new Date(assignments[0].assignedAt);
const lastAssignment = assignments[assignments.length - 1].unassignedAt;
const unassignedAt = lastAssignment ? new Date(lastAssignment) : new Date();
return commentDate >= assignedAt && commentDate <= unassignedAt;
}
return assignments.some((period) => {
const assignedAt = new Date(period.assignedAt);
const unassignedAt = period.unassignedAt ? new Date(period.unassignedAt) : new Date();
return commentDate >= assignedAt && commentDate <= unassignedAt;
});
}
34 changes: 31 additions & 3 deletions src/parser/data-purge-module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { DataPurgeConfiguration } from "../configuration/data-purge-config";
import { GitHubPullRequestReviewComment } from "../github-types";
import { getAssignmentPeriods, isCommentDuringAssignment, UserAssignments } from "../helpers/user-assigned-timespan";
import { IssueActivity } from "../issue-activity";
import { parseGitHubUrl } from "../start";
import { BaseModule } from "../types/module";
import { Result } from "../types/results";

Expand All @@ -9,6 +11,7 @@ import { Result } from "../types/results";
*/
export class DataPurgeModule extends BaseModule {
readonly _configuration: DataPurgeConfiguration | null = this.context.config.incentives.dataPurge;
_assignmentPeriods: UserAssignments = {};

get enabled(): boolean {
if (!this._configuration) {
Expand All @@ -18,11 +21,36 @@ export class DataPurgeModule extends BaseModule {
return true;
}

async _shouldSkipComment(comment: IssueActivity["allComments"][0]) {
if ("isMinimized" in comment && comment.isMinimized) {
this.context.logger.debug("Skipping hidden comment", { comment });
return true;
}
if (
this._configuration?.skipCommentsWhileAssigned &&
this._configuration.skipCommentsWhileAssigned !== "none" &&
comment.user?.login &&
isCommentDuringAssignment(
comment,
this._assignmentPeriods[comment.user?.login],
this._configuration.skipCommentsWhileAssigned === "exact"
)
) {
this.context.logger.debug("Skipping comment during assignment", {
comment,
});
return true;
}
return false;
}

async transform(data: Readonly<IssueActivity>, result: Result) {
this._assignmentPeriods = await getAssignmentPeriods(
this.context.octokit,
parseGitHubUrl(this.context.payload.issue.html_url)
);
for (const comment of data.allComments) {
// Skips comments if they are minimized
if ("isMinimized" in comment && comment.isMinimized) {
this.context.logger.debug("Skipping hidden comment", { comment });
if (await this._shouldSkipComment(comment)) {
continue;
}
if (comment.body && comment.user?.login && result[comment.user.login]) {
Expand Down
17 changes: 13 additions & 4 deletions src/parser/github-comment-module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ export class GithubCommentModule extends BaseModule {
async getBodyContent(result: Result, stripContent = false): Promise<string> {
const keysToRemove: string[] = [];
const bodyArray: (string | undefined)[] = [];
const taskReward = Object.values(result).reduce((acc, curr) => {
if (curr.task) {
return curr.task.reward * curr.task.multiplier;
}
return acc;
}, 0);

if (stripContent) {
this.context.logger.info("Stripping content due to excessive length.");
Expand All @@ -52,7 +58,7 @@ export class GithubCommentModule extends BaseModule {
for (const [key, value] of Object.entries(result)) {
// Remove result with 0 total from being displayed
if (result[key].total <= 0) continue;
result[key].evaluationCommentHtml = await this._generateHtml(key, value, true);
result[key].evaluationCommentHtml = await this._generateHtml(key, value, taskReward, true);
bodyArray.push(result[key].evaluationCommentHtml);
}
bodyArray.push(
Expand All @@ -69,7 +75,7 @@ export class GithubCommentModule extends BaseModule {
keysToRemove.push(key);
continue;
}
result[key].evaluationCommentHtml = await this._generateHtml(key, value);
result[key].evaluationCommentHtml = await this._generateHtml(key, value, taskReward);
bodyArray.push(result[key].evaluationCommentHtml);
}
// Remove evaluationCommentHtml because it is superfluous
Expand Down Expand Up @@ -255,7 +261,7 @@ export class GithubCommentModule extends BaseModule {
return content.join("");
}

async _generateHtml(username: string, result: Result[0], stripComments = false) {
async _generateHtml(username: string, result: Result[0], taskReward: number, stripComments = false) {
const sortedTasks = result.comments?.reduce<SortedTasks>(
(acc, curr) => {
if (curr.type & CommentKind.ISSUE) {
Expand All @@ -277,6 +283,9 @@ export class GithubCommentModule extends BaseModule {
this.context.config.erc20RewardToken
);

const rewardsSum = result.comments?.reduce<number>((acc, curr) => acc + (curr.score?.reward ?? 0), 0) ?? 0;
const isCapped = result.total < rewardsSum;

return `
<details>
<summary>
Expand All @@ -294,6 +303,7 @@ export class GithubCommentModule extends BaseModule {
</b>
</summary>
${result.feeRate !== undefined ? `<h6>⚠️ ${new Decimal(result.feeRate).mul(100)}% fee rate has been applied. Consider using the&nbsp;<a href="https://dao.ubq.fi/dollar" target="_blank" rel="noopener">Ubiquity Dollar</a>&nbsp;for no fees.</h6>` : ""}
${isCapped ? `<h6>⚠️ Your rewards have been limited to the task price of ${taskReward} ${tokenSymbol}.</h6>` : ""}
<h6>Contributions Overview</h6>
<table>
<thead>
Expand All @@ -302,7 +312,6 @@ export class GithubCommentModule extends BaseModule {
<th>Contribution</th>
<th>Count</th>
<th>Reward</th>
</tr>
</thead>
<tbody>
Expand Down
59 changes: 59 additions & 0 deletions tests/process.issue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ import permitGenerationResults from "./__mocks__/results/permit-generation-resul
import userCommentResults from "./__mocks__/results/user-comment-results.json";
import cfg from "./__mocks__/results/valid-configuration.json";
import { customOctokit as Octokit } from "@ubiquity-os/plugin-sdk/octokit";
import { CommentAssociation } from "../src/configuration/comment-types";

const issueUrl = process.env.TEST_ISSUE_URL ?? "https://github.com/ubiquity-os/conversation-rewards/issues/5";

Expand Down Expand Up @@ -363,4 +364,62 @@ describe("Modules tests", () => {
},
});
});

it("Should not limit the assigned user", async () => {
const processor = new Processor({
...ctx,
config: {
...ctx.config,
incentives: {
...ctx.config.incentives,
limitRewards: true,
},
},
});
processor["_transformers"] = [
new UserExtractorModule(ctx),
new DataPurgeModule(ctx),
new FormattingEvaluatorModule(ctx),
new ContentEvaluatorModule(ctx),
];
processor["_result"] = {
gentlementlegen: {
total: 999,
task: {
multiplier: 0.5,
reward: 18.5,
},
comments: [
{
id: 1,
content: "",
url: "",
type: CommentAssociation.ASSIGNEE,
score: {
reward: 50000,
multiplier: 3,
},
},
],
userId: 9807008,
},
user2: {
total: 11111111,
userId: 1,
},
};
const result = processor._getRewardsLimit();
expect(result).toBe(9.25);
const total = await processor.run(activity);
expect(total).toMatchObject({
gentlementlegen: { total: 800, task: { multiplier: 1, reward: 400 }, userId: 9807008 },
user2: { total: 0, userId: 1 },
"0x4007": {
total: 400,
},
whilefoo: {
total: 43.562,
},
});
});
});

0 comments on commit 14bfd78

Please sign in to comment.