Skip to content

Commit

Permalink
Merge pull request #199 from gentlementlegen/feat/filter-comments-dur…
Browse files Browse the repository at this point in the history
…ing-assignment

feat: filter comments during assignment
  • Loading branch information
gentlementlegen authored Dec 2, 2024
2 parents 4810e4d + 64c7463 commit d0f0921
Show file tree
Hide file tree
Showing 8 changed files with 146 additions and 8 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
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
2 changes: 1 addition & 1 deletion dist/index.js

Large diffs are not rendered by default.

22 changes: 21 additions & 1 deletion manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -268,7 +268,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
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

0 comments on commit d0f0921

Please sign in to comment.