Skip to content

Commit

Permalink
Merge pull request ubiquity-os-marketplace#24 from gentlementlegen/fe…
Browse files Browse the repository at this point in the history
…at/notify-self-assign

feat: notify self assign
  • Loading branch information
gentlementlegen authored Sep 7, 2024
2 parents ddd787b + c79b63f commit 2be6491
Show file tree
Hide file tree
Showing 11 changed files with 118 additions and 72 deletions.
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
10 changes: 10 additions & 0 deletions src/handlers/result-types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
export enum HttpStatusCode {
OK = 200,
NOT_MODIFIED = 304,
}

export interface Result {
status: HttpStatusCode;
content?: string;
reason?: string;
}
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 "../result-types";
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);
}

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 "../result-types";

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 "./result-types";
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 };
}
35 changes: 19 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 { userSelfAssign, userStartStop } from "./handlers/user-start-stop";
import { Context, Env, PluginInputs } from "./types";
import { addCommentToIssue } from "./utils/issue";

Expand All @@ -22,22 +22,25 @@ 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 {
switch (context.eventName) {
case "issue_comment.created":
return await userStartStop(context);
case "issues.assigned":
return await userSelfAssign(context);
default:
context.logger.error(`Unsupported event: ${context.eventName}`);
}
} else {
context.logger.error(`Unsupported event: ${context.eventName}`);
} 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-->`);
}
}

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

0 comments on commit 2be6491

Please sign in to comment.