Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: notify self assign #24

Merged
Show file tree
Hide file tree
Changes from 85 commits
Commits
Show all changes
86 commits
Select commit Hold shift + click to select a range
2cce223
Merge pull request #2 from Meniole/development
gentlementlegen Jul 7, 2024
48203fe
chore: removed yarn 4
gentlementlegen Jul 7, 2024
ba529e1
chore: renamed wrangler.toml
gentlementlegen Jul 7, 2024
d3226ca
chore: default enabled true
gentlementlegen Jul 7, 2024
e0f9047
chore: added manifest.json
gentlementlegen Jul 10, 2024
dad7b2b
chore: added manifest.json
gentlementlegen Jul 10, 2024
0c40f37
chore: added manifest.json
gentlementlegen Jul 10, 2024
e4fee10
chore: manifest.json
gentlementlegen Jul 11, 2024
acb228c
chore: manifest.json
gentlementlegen Jul 12, 2024
565fd1c
feat: max task assignment for collaborators
Jul 14, 2024
a74d31b
feat: make task limits configurable
Jul 15, 2024
b30416e
Merge pull request #3 from Meniole/development
gentlementlegen Jul 17, 2024
8017e89
Merge branch 'refs/heads/development' into meniole-main
gentlementlegen Jul 17, 2024
cddec1e
chore: manifest.json
gentlementlegen Jul 17, 2024
c36730a
Merge remote-tracking branch 'meniole/main' into meniole-main
gentlementlegen Jul 17, 2024
a1508fc
fix: get contributors from config
Jul 22, 2024
f5199d8
fix: remove union type
Jul 22, 2024
df7770e
fix: return lowest task limit if user role doesnt match any of those …
Jul 23, 2024
661fb85
chore: code cleanups
Jul 23, 2024
b2ec2f6
test: add test to cover functionality
Jul 23, 2024
14f7c33
chore: fix merge conflicts
Jul 23, 2024
05f43ed
fix: pass correct inputs to getUserRole
Jul 23, 2024
71e1c1a
test: add test
Jul 29, 2024
06294ca
test: cleanups
Jul 29, 2024
ec2e5bb
test: made requested changes
Jul 30, 2024
38be206
test: cleanpup test
Jul 30, 2024
468cd49
fix: set smallest task on error when getting user role
Jul 30, 2024
91984c7
chore: fix merge conflicts
Jul 30, 2024
86accef
fix: refactor test to use the correct format
Jul 31, 2024
1927aed
fix: add db issues to cover all test
Jul 31, 2024
a06ab85
fix: assignee issue and open pr fetching
Keyrxng Jul 31, 2024
e343a6d
chore: fix filter and use math.abs
Keyrxng Jul 31, 2024
09c1091
chore: tests
Keyrxng Jul 31, 2024
c341006
Merge branch 'max-assignments' into fix/max-and-assigned
Keyrxng Jul 31, 2024
ca1f0c5
chore: ci
Keyrxng Jul 31, 2024
944883d
Merge pull request #21 from ubq-testing/fix/max-and-assigned
jordan-ae Jul 31, 2024
dcddd3e
chore: rename file to match return
jordan-ae Aug 5, 2024
8221f51
fix: use logger to display message
jordan-ae Aug 6, 2024
0d55cb1
Update src/handlers/shared/start.ts
jordan-ae Aug 12, 2024
101c1f8
chore: rename function to match return
jordan-ae Aug 12, 2024
0084bcb
chore: address code reviews
jordan-ae Aug 12, 2024
92ce9d4
fix: clean up queries
jordan-ae Aug 14, 2024
665fd5c
Merge branch 'fork/ubq-test-jordan/max-assignments' into meniole-main
gentlementlegen Aug 14, 2024
c8a018d
feat: user self assign message display
gentlementlegen Aug 20, 2024
cffb784
Merge branch 'feat/notify-self-assign' into meniole-main
gentlementlegen Aug 20, 2024
c3fc7ca
Merge remote-tracking branch 'meniole/main' into meniole-main
gentlementlegen Aug 20, 2024
d0a00d8
chore: fixed event name
gentlementlegen Aug 20, 2024
6fc8f09
chore: simplified deadline calculation
gentlementlegen Aug 20, 2024
2bf7363
feat: check unassigns
Jul 15, 2024
1f82df8
feat: previous assignment filter
Keyrxng Jul 28, 2024
7310c50
chore: make bot name match configurable
Keyrxng Jul 28, 2024
b69f4c3
chore: tests and configurable botUsernames
Keyrxng Jul 28, 2024
9089fe3
chore: remove botUsernames item
Keyrxng Jul 29, 2024
316ca9e
chore: fix test
Keyrxng Jul 29, 2024
f3b1e52
chore: minor fixes
Keyrxng Jul 30, 2024
671be3d
chore: refactor tests
Keyrxng Jul 30, 2024
bedb60b
feat: max task assignment for collaborators
Jul 14, 2024
71324b8
feat: make task limits configurable
Jul 15, 2024
212f9bf
fix: get contributors from config
Jul 22, 2024
391bdde
fix: return lowest task limit if user role doesnt match any of those …
Jul 23, 2024
bf6e7da
chore: code cleanups
Jul 23, 2024
da1b51d
test: add test to cover functionality
Jul 23, 2024
83a6941
test: cleanups
Jul 29, 2024
9005f01
test: made requested changes
Jul 30, 2024
10b985a
fix: set smallest task on error when getting user role
Jul 30, 2024
73119b5
chore: fix filter and use math.abs
Keyrxng Jul 31, 2024
0753f36
chore: rename file to match return
jordan-ae Aug 5, 2024
42cdf6e
chore: rename function to match return
jordan-ae Aug 12, 2024
535ec00
chore: address code reviews
jordan-ae Aug 12, 2024
df7ff28
fix: clean up queries
jordan-ae Aug 14, 2024
c148fba
chore: merging
gentlementlegen Aug 27, 2024
4fe61f3
chore: merge changes
gentlementlegen Sep 1, 2024
dd7eb09
chore: merge changes
gentlementlegen Sep 1, 2024
eb37a3d
Update src/handlers/shared/start.ts
gentlementlegen Sep 1, 2024
1977116
chore: merge changes
gentlementlegen Sep 1, 2024
d7ca019
chore: merge changes
gentlementlegen Sep 1, 2024
615d16a
chore: merge changes
gentlementlegen Sep 1, 2024
68890ac
chore: merge changes
gentlementlegen Sep 1, 2024
22b64c4
chore: fixed test
gentlementlegen Sep 1, 2024
5437e76
chore: fixed test
gentlementlegen Sep 1, 2024
10650d8
chore: fixed test
gentlementlegen Sep 2, 2024
b8cd7af
chore: changed status code
gentlementlegen Sep 2, 2024
3764eea
chore: fixed tests
gentlementlegen Sep 2, 2024
0dc082b
Merge branch 'development' into feat/notify-self-assign
gentlementlegen Sep 3, 2024
ebf98c4
fix: skipping deadline self assign post if no deadline
gentlementlegen Sep 3, 2024
c79b63f
fix: the messages are displayed on catch errors
gentlementlegen Sep 4, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion manifest.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "Start | Stop",
"description": "Assign or un-assign yourself from an issue.",
"ubiquity:listeners": ["issue_comment.created"],
"ubiquity:listeners": ["issue_comment.created", "issues.assigned"],
"commands": {
"start": {
"ubiquity:example": "/start",
Expand Down
30 changes: 30 additions & 0 deletions src/handlers/proxy.ts
Copy link
Member

Choose a reason for hiding this comment

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

What is this file?

Copy link
Member Author

Choose a reason for hiding this comment

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

It contains proxy calls for events.

Copy link
Member

Choose a reason for hiding this comment

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

What is a "proxy call"?

Copy link
Member Author

Choose a reason for hiding this comment

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

Copy link
Member

Choose a reason for hiding this comment

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

I would appreciate comments in this file and I think newer contributors would too. The first time I saw it in another plugin I thought "this looks like heavy artillery"

Copy link
Member

Choose a reason for hiding this comment

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

• get is a trap for the get operation (i.e., property access). When you access a property on proxy, the trap intercepts the operation.

https://chatgpt.com/share/915e1875-a26a-4deb-b9fc-705e59f0212a

Copy link
Member

@Keyrxng Keyrxng Aug 20, 2024

Choose a reason for hiding this comment

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

• get is a trap for the get operation (i.e., property access). When you access a property on proxy, the trap intercepts the operation.

https://chatgpt.com/share/915e1875-a26a-4deb-b9fc-705e59f0212a

But all this proxy does is convolute what could be a simple if or switch statement as we do not perform any mutation or action within the proxy other than calling one of two functions. rpc-handler uses a Proxy but it does mutate and control logic so it makes sense there, here it seems very OP.

I guess my point is, are we adopting this approach across plugins as standard rather than the previous method of using an if for three SupportedEvents or less and a switch for more than thee?

Copy link
Member Author

Choose a reason for hiding this comment

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

Sure a switch can work. Having the proxy enforces implementing all the supported events that would be the biggest difference.

Copy link
Member

Choose a reason for hiding this comment

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

I am not against the Proxy but I think if we are adopting it as standard we should comment it clearly so that no-one else has to ask GPT what's going here

Having the proxy enforces implementing all the supported events that would be the biggest difference.

This is a good reason for it.


I cannot shake the thought: if events are arbitrary and not defined in our SupportedEvents the switch and the Proxy will not run.

If plugins are built to work on any event like user-activity-watcher I think you said @gentlementlegen then the proxy would need to handle all events.

Plugins which cannot run on any event need to be restricted to only their supported events at the plugin config level I think.

Copy link
Member Author

Choose a reason for hiding this comment

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

I can make it a switch case, I just think that the more actions we should handle the more the switch case will be massive so that was to make code lighter. The proxy is needed because eventually the called key might not be in the object. In OOP it would just be a pointer to function object, which would be less fancy.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { Context, Env, SupportedEventsU } from "../types";
import { userSelfAssign, userStartStop } from "./user-start-stop";

export enum HttpStatusCode {
OK = 200,
NOT_MODIFIED = 304,
}

export interface Result {
status: HttpStatusCode;
content?: string;
reason?: string;
}

const callbacks: { [k in SupportedEventsU]: (context: Context, env: Env) => Result | Promise<Result> } = {
"issues.assigned": userSelfAssign,
"issue_comment.created": userStartStop,
};

export function proxyCallbacks({ logger }: Context) {
return new Proxy(callbacks, {
get(target, prop: SupportedEventsU) {
if (!(prop in target)) {
logger.error(`${prop} is not supported, skipping.`);
return async () => ({ status: "skipped", reason: "unsupported_event" });
}
return target[prop].bind(target);
},
});
}
24 changes: 16 additions & 8 deletions src/handlers/shared/generate-assignment-comment.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { Context } from "../../types/context";
import { Context } from "../../types";
import { calculateDurations } from "../../utils/shared";

const options: Intl.DateTimeFormatOptions = {
export const options: Intl.DateTimeFormatOptions = {
weekday: "short",
month: "short",
day: "numeric",
Expand All @@ -10,16 +11,23 @@ const options: Intl.DateTimeFormatOptions = {
timeZoneName: "short",
};

export async function generateAssignmentComment(context: Context, issueCreatedAt: string, issueNumber: number, senderId: number, duration: number) {
export function getDeadline(issue: Context["payload"]["issue"]): string | null {
if (!issue?.labels) {
throw new Error("No labels are set.");
}
const startTime = new Date().getTime();
const duration: number = calculateDurations(issue.labels).shift() ?? 0;
if (!duration) return null;
const endTime = new Date(startTime + duration * 1000);
return endTime.toLocaleString("en-US", options);
}

export async function generateAssignmentComment(context: Context, issueCreatedAt: string, issueNumber: number, senderId: number, deadline: string | null) {
const startTime = new Date().getTime();
let endTime: null | Date = null;
let deadline: null | string = null;
endTime = new Date(startTime + duration * 1000);
deadline = endTime.toLocaleString("en-US", options);

return {
daysElapsedSinceTaskCreation: Math.floor((startTime - new Date(issueCreatedAt).getTime()) / 1000 / 60 / 60 / 24),
deadline: duration > 0 ? deadline : null,
deadline: deadline ?? null,
registeredWallet:
(await context.adapters.supabase.user.getWalletByUserId(senderId, issueNumber)) ||
"Register your wallet address using the following slash command: `/wallet 0x0000...0000`",
Expand Down
18 changes: 9 additions & 9 deletions src/handlers/shared/start.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import { Context, ISSUE_TYPE, Label } from "../../types";
import { isParentIssue, getAvailableOpenedPullRequests, getAssignedIssues, addAssignees, addCommentToIssue, getTimeValue } from "../../utils/issue";
import { calculateDurations } from "../../utils/shared";
import { checkTaskStale } from "./check-task-stale";
import { addAssignees, addCommentToIssue, getAssignedIssues, getAvailableOpenedPullRequests, getTimeValue, isParentIssue } from "../../utils/issue";
import { HttpStatusCode, Result } from "../proxy";
import { hasUserBeenUnassigned } from "./check-assignments";
import { generateAssignmentComment } from "./generate-assignment-comment";
import { checkTaskStale } from "./check-task-stale";
import { generateAssignmentComment, getDeadline } from "./generate-assignment-comment";
import structuredMetadata from "./structured-metadata";
import { assignTableComment } from "./table";

export async function start(context: Context, issue: Context["payload"]["issue"], sender: Context["payload"]["sender"], teammates: string[]) {
export async function start(context: Context, issue: Context["payload"]["issue"], sender: Context["payload"]["sender"], teammates: string[]): Promise<Result> {
const { logger, config } = context;
const { maxConcurrentTasks, taskStaleTimeoutDuration } = config;

Expand Down Expand Up @@ -75,17 +75,17 @@ export async function start(context: Context, issue: Context["payload"]["issue"]
}

// get labels
const labels = issue.labels;
const labels = issue.labels ?? [];
const priceLabel = labels.find((label: Label) => label.name.startsWith("Price: "));

if (!priceLabel) {
throw new Error(logger.error("No price label is set to calculate the duration", { issueNumber: issue.number }).logMessage.raw);
gentlementlegen marked this conversation as resolved.
Show resolved Hide resolved
}

const duration: number = calculateDurations(labels).shift() ?? 0;
const deadline = getDeadline(issue);
const toAssignIds = await fetchUserIds(context, toAssign);

const assignmentComment = await generateAssignmentComment(context, issue.created_at, issue.number, sender.id, duration);
const assignmentComment = await generateAssignmentComment(context, issue.created_at, issue.number, sender.id, deadline);
const logMessage = logger.info("Task assigned successfully", {
taskDeadline: assignmentComment.deadline,
taskAssignees: toAssignIds,
Expand Down Expand Up @@ -113,7 +113,7 @@ export async function start(context: Context, issue: Context["payload"]["issue"]
].join("\n") as string
);

return { output: "Task assigned successfully" };
return { content: "Task assigned successfully", status: HttpStatusCode.OK };
}

async function fetchUserIds(context: Context, username: string[]) {
Expand Down
5 changes: 3 additions & 2 deletions src/handlers/shared/stop.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { Assignee, Context, Sender } from "../../types";
import { addCommentToIssue, closePullRequestForAnIssue } from "../../utils/issue";
import { HttpStatusCode, Result } from "../proxy";

export async function stop(context: Context, issue: Context["payload"]["issue"], sender: Sender, repo: Context["payload"]["repository"]) {
export async function stop(context: Context, issue: Context["payload"]["issue"], sender: Sender, repo: Context["payload"]["repository"]): Promise<Result> {
const { logger } = context;
const issueNumber = issue.number;

Expand Down Expand Up @@ -47,5 +48,5 @@ export async function stop(context: Context, issue: Context["payload"]["issue"],
});

await addCommentToIssue(context, unassignedLog?.logMessage.diff as string);
return { output: "Task unassigned successfully" };
return { content: "Task unassigned successfully", status: HttpStatusCode.OK };
}
32 changes: 26 additions & 6 deletions src/handlers/user-start-stop.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,14 @@
import { Context } from "../types";
import { Context, isContextCommentCreated } from "../types";
import { addCommentToIssue } from "../utils/issue";
import { HttpStatusCode, Result } from "./proxy";
import { getDeadline } from "./shared/generate-assignment-comment";
import { start } from "./shared/start";
import { stop } from "./shared/stop";

export async function userStartStop(context: Context): Promise<{ output: string | null }> {
export async function userStartStop(context: Context): Promise<Result> {
if (!isContextCommentCreated(context)) {
return { status: HttpStatusCode.NOT_MODIFIED };
}
const { payload } = context;
const { issue, comment, sender, repository } = payload;
const slashCommand = comment.body.split(" ")[0].replace("/", "");
Expand All @@ -11,13 +17,27 @@ export async function userStartStop(context: Context): Promise<{ output: string
.slice(1)
.map((teamMate) => teamMate.split(" ")[0]);

const user = comment.user?.login ? { login: comment.user.login, id: comment.user.id } : { login: sender.login, id: sender.id };

if (slashCommand === "stop") {
return await stop(context, issue, user, repository);
return await stop(context, issue, sender, repository);
} else if (slashCommand === "start") {
return await start(context, issue, sender, teamMates);
}

return { output: null };
return { status: HttpStatusCode.NOT_MODIFIED };
}

export async function userSelfAssign(context: Context): Promise<Result> {
const { payload } = context;
const { issue } = payload;
const deadline = getDeadline(issue);

if (!deadline) {
context.logger.debug("Skipping deadline posting message because no deadline has been set.");
return { status: HttpStatusCode.NOT_MODIFIED };
}

const users = issue.assignees.map((user) => `@${user?.login}`).join(", ");

await addCommentToIssue(context, `${users} the deadline is at ${deadline}`);
return { status: HttpStatusCode.OK };
}
28 changes: 12 additions & 16 deletions src/plugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { Octokit } from "@octokit/rest";
import { createClient } from "@supabase/supabase-js";
import { LogReturn, Logs } from "@ubiquity-dao/ubiquibot-logger";
import { createAdapters } from "./adapters";
import { userStartStop } from "./handlers/user-start-stop";
import { proxyCallbacks } from "./handlers/proxy";
import { Context, Env, PluginInputs } from "./types";
import { addCommentToIssue } from "./utils/issue";

Expand All @@ -22,22 +22,18 @@ export async function startStopTask(inputs: PluginInputs, env: Env) {

context.adapters = createAdapters(supabase, context);

if (context.eventName === "issue_comment.created") {
try {
return await userStartStop(context);
} catch (err) {
let errorMessage;
if (err instanceof LogReturn) {
errorMessage = err;
} else if (err instanceof Error) {
errorMessage = context.logger.error(err.message, { error: err });
} else {
errorMessage = context.logger.error("An error occurred", { err });
}
await addCommentToIssue(context, `${errorMessage?.logMessage.diff}\n<!--\n${sanitizeMetadata(errorMessage?.metadata)}\n-->`);
try {
return proxyCallbacks(context)[inputs.eventName](context, env);
} catch (err) {
let errorMessage;
if (err instanceof LogReturn) {
errorMessage = err;
} else if (err instanceof Error) {
errorMessage = context.logger.error(err.message, { error: err });
} else {
errorMessage = context.logger.error("An error occurred", { err });
}
} else {
context.logger.error(`Unsupported event: ${context.eventName}`);
await addCommentToIssue(context, `${errorMessage?.logMessage.diff}\n<!--\n${sanitizeMetadata(errorMessage?.metadata)}\n-->`);
}
}

Expand Down
6 changes: 5 additions & 1 deletion src/types/context.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,12 +5,16 @@ import { createAdapters } from "../adapters";
import { Env } from "./env";
import { Logs } from "@ubiquity-dao/ubiquibot-logger";

export type SupportedEventsU = "issue_comment.created";
export type SupportedEventsU = "issue_comment.created" | "issues.assigned";

export type SupportedEvents = {
[K in SupportedEventsU]: K extends WebhookEventName ? WebhookEvent<K> : never;
};

export function isContextCommentCreated(context: Context): context is Context<"issue_comment.created"> {
return "comment" in context.payload;
}

export interface Context<T extends SupportedEventsU = SupportedEventsU, TU extends SupportedEvents[T] = SupportedEvents[T]> {
eventName: T;
payload: TU["payload"];
Expand Down
2 changes: 1 addition & 1 deletion src/worker.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Value } from "@sinclair/typebox/value";
import manifest from "../manifest.json";
import { startStopTask } from "./plugin";
import { Env, envConfigValidator, startStopSchema, startStopSettingsValidator } from "./types";
import manifest from "../manifest.json";

export default {
async fetch(request: Request, env: Env): Promise<Response> {
Expand Down
Loading