diff --git a/package.json b/package.json index 7c3fc1ecd..b0674d1d5 100644 --- a/package.json +++ b/package.json @@ -64,6 +64,7 @@ "probot": "^12.2.4", "smee-client": "^2.0.0", "tsx": "^3.12.7", + "ubiquibot-logger": "^0.3.5", "yaml": "^2.2.2", "zlib": "^1.0.5" }, diff --git a/src/adapters/adapters.ts b/src/adapters/adapters.ts index c3857d53a..a0ccb9592 100644 --- a/src/adapters/adapters.ts +++ b/src/adapters/adapters.ts @@ -2,7 +2,7 @@ import { createClient } from "@supabase/supabase-js"; import { Access } from "./supabase/helpers/tables/access"; import { Label } from "./supabase/helpers/tables/label"; import { Locations } from "./supabase/helpers/tables/locations"; -import { Logs } from "./supabase/helpers/tables/logs"; +import { Logs } from "ubiquibot-logger"; import { Settlement } from "./supabase/helpers/tables/settlement"; import { Super } from "./supabase/helpers/tables/super"; import { User } from "./supabase/helpers/tables/user"; diff --git a/src/adapters/supabase/helpers/pretty-logs.ts b/src/adapters/supabase/helpers/pretty-logs.ts deleted file mode 100644 index 0e02046c8..000000000 --- a/src/adapters/supabase/helpers/pretty-logs.ts +++ /dev/null @@ -1,188 +0,0 @@ -import util from "util"; -/* eslint-disable @typescript-eslint/no-unused-vars */ -/* eslint-disable @typescript-eslint/no-explicit-any */ -type PrettyLogsWithOk = "ok" | LogLevel; -export class PrettyLogs { - constructor() { - this.ok = this.ok.bind(this); - this.info = this.info.bind(this); - this.error = this.error.bind(this); - this.fatal = this.fatal.bind(this); - this.debug = this.debug.bind(this); - this.verbose = this.verbose.bind(this); - } - public fatal(message: string, metadata?: any) { - this._logWithStack(LogLevel.FATAL, message, metadata); - } - - public error(message: string, metadata?: any) { - this._logWithStack(LogLevel.ERROR, message, metadata); - } - - public ok(message: string, metadata?: any) { - this._logWithStack("ok", message, metadata); - } - - public info(message: string, metadata?: any) { - this._logWithStack(LogLevel.INFO, message, metadata); - } - - public debug(message: string, metadata?: any) { - this._logWithStack(LogLevel.DEBUG, message, metadata); - } - - public verbose(message: string, metadata?: any) { - this._logWithStack(LogLevel.VERBOSE, message, metadata); - } - - private _logWithStack(type: "ok" | LogLevel, message: string, metadata?: Metadata | string) { - this._log(type, message); - if (typeof metadata === "string") { - this._log(type, metadata); - return; - } - if (metadata) { - let stack = metadata?.error?.stack || metadata?.stack; - if (!stack) { - // generate and remove the top four lines of the stack trace - const stackTrace = new Error().stack?.split("\n"); - if (stackTrace) { - stackTrace.splice(0, 4); - stack = stackTrace.filter((line) => line.includes(".ts:")).join("\n"); - } - } - const newMetadata = { ...metadata }; - delete newMetadata.message; - delete newMetadata.name; - delete newMetadata.stack; - - if (!this._isEmpty(newMetadata)) { - this._log(type, newMetadata); - } - - if (typeof stack == "string") { - const prettyStack = this._formatStackTrace(stack, 1); - const colorizedStack = this._colorizeText(prettyStack, Colors.dim); - this._log(type, colorizedStack); - } else if (stack) { - const prettyStack = this._formatStackTrace((stack as unknown as string[]).join("\n"), 1); - const colorizedStack = this._colorizeText(prettyStack, Colors.dim); - this._log(type, colorizedStack); - } else { - throw new Error("Stack is null"); - } - } - } - - private _colorizeText(text: string, color: Colors): string { - if (!color) { - throw new Error(`Invalid color: ${color}`); - } - return color.concat(text).concat(Colors.reset); - } - - private _formatStackTrace(stack: string, linesToRemove = 0, prefix = ""): string { - const lines = stack.split("\n"); - for (let i = 0; i < linesToRemove; i++) { - lines.shift(); // Remove the top line - } - return lines - .map((line) => `${prefix}${line.replace(/\s*at\s*/, " ↳ ")}`) // Replace 'at' and prefix every line - .join("\n"); - } - - private _isEmpty(obj: Record) { - return !Reflect.ownKeys(obj).some((key) => typeof obj[String(key)] !== "function"); - } - - private _log(type: PrettyLogsWithOk, message: any) { - const defaultSymbols: Record = { - fatal: "×", - ok: "✓", - error: "⚠", - info: "›", - debug: "››", - verbose: "💬", - }; - - const symbol = defaultSymbols[type]; - - // Formatting the message - const messageFormatted = - typeof message === "string" - ? message - : util.inspect(message, { showHidden: true, depth: null, breakLength: Infinity }); - // const messageFormatted = - // typeof message === "string" ? message : JSON.stringify(Logs.convertErrorsIntoObjects(message)); - - // Constructing the full log string with the prefix symbol - const lines = messageFormatted.split("\n"); - const logString = lines - .map((line, index) => { - // Add the symbol only to the first line and keep the indentation for the rest - const prefix = index === 0 ? `\t${symbol}` : `\t${" ".repeat(symbol.length)}`; - return `${prefix} ${line}`; - }) - .join("\n"); - - const fullLogString = logString; - - const colorMap: Record = { - fatal: ["error", Colors.fgRed], - ok: ["log", Colors.fgGreen], - error: ["warn", Colors.fgYellow], - info: ["info", Colors.dim], - debug: ["debug", Colors.fgMagenta], - verbose: ["debug", Colors.dim], - }; - - const _console = console[colorMap[type][0] as keyof typeof console] as (...args: string[]) => void; - if (typeof _console === "function") { - _console(this._colorizeText(fullLogString, colorMap[type][1])); - } else { - throw new Error(fullLogString); - } - } -} -interface Metadata { - error?: { stack?: string }; - stack?: string; - message?: string; - name?: string; - [key: string]: any; -} - -enum Colors { - reset = "\x1b[0m", - bright = "\x1b[1m", - dim = "\x1b[2m", - underscore = "\x1b[4m", - blink = "\x1b[5m", - reverse = "\x1b[7m", - hidden = "\x1b[8m", - - fgBlack = "\x1b[30m", - fgRed = "\x1b[31m", - fgGreen = "\x1b[32m", - fgYellow = "\x1b[33m", - fgBlue = "\x1b[34m", - fgMagenta = "\x1b[35m", - fgCyan = "\x1b[36m", - fgWhite = "\x1b[37m", - - bgBlack = "\x1b[40m", - bgRed = "\x1b[41m", - bgGreen = "\x1b[42m", - bgYellow = "\x1b[43m", - bgBlue = "\x1b[44m", - bgMagenta = "\x1b[45m", - bgCyan = "\x1b[46m", - bgWhite = "\x1b[47m", -} -export enum LogLevel { - FATAL = "fatal", - ERROR = "error", - INFO = "info", - VERBOSE = "verbose", - DEBUG = "debug", -} diff --git a/src/adapters/supabase/helpers/tables/logs.ts b/src/adapters/supabase/helpers/tables/logs.ts deleted file mode 100644 index bb83a0f0f..000000000 --- a/src/adapters/supabase/helpers/tables/logs.ts +++ /dev/null @@ -1,376 +0,0 @@ -/* eslint-disable @typescript-eslint/no-explicit-any */ -// This is disabled because logs should be able to log any type of data -// Normally this is forbidden - -import { SupabaseClient } from "@supabase/supabase-js"; -import { Context as ProbotContext } from "probot"; -import Runtime from "../../../../bindings/bot-runtime"; -import { COMMIT_HASH } from "../../../../commit-hash"; -import { Database } from "../../types/database"; - -import { LogLevel, PrettyLogs } from "../pretty-logs"; - -type LogFunction = (message: string, metadata?: any) => void; -type LogInsert = Database["public"]["Tables"]["logs"]["Insert"]; -type LogParams = { - level: LogLevel; - consoleLog: LogFunction; - logMessage: string; - metadata?: any; - postComment?: boolean; - type: PublicMethods; -}; -export class LogReturn { - logMessage: LogMessage; - metadata?: any; - - constructor(logMessage: LogMessage, metadata?: any) { - this.logMessage = logMessage; - this.metadata = metadata; - } -} - -type FunctionPropertyNames = { - [K in keyof T]: T[K] extends (...args: any[]) => any ? K : never; -}[keyof T]; - -type PublicMethods = Exclude, "constructor" | keyof object>; - -export type LogMessage = { raw: string; diff: string; level: LogLevel; type: PublicMethods }; - -export class Logs { - private _supabase: SupabaseClient; - private _context: ProbotContext | null = null; - - private _maxLevel = -1; - private _queue: LogInsert[] = []; // Your log queue - private _concurrency = 6; // Maximum concurrent requests - private _retryDelay = 1000; // Delay between retries in milliseconds - private _throttleCount = 0; - private _retryLimit = 0; // Retries disabled by default - - static console: PrettyLogs; - - private _log({ level, consoleLog, logMessage, metadata, postComment, type }: LogParams): LogReturn | null { - if (this._getNumericLevel(level) > this._maxLevel) return null; // filter out more verbose logs according to maxLevel set in config - - // needs to generate three versions of the information. - // they must all first serialize the error object if it exists - // - the comment to post on supabase (must be raw) - // - the comment to post on github (must include diff syntax) - // - the comment to post on the console (must be colorized) - - consoleLog(logMessage, metadata || undefined); - - if (this._context && postComment) { - const colorizedCommentMessage = this._diffColorCommentMessage(type, logMessage); - const commentMetaData = metadata ? Logs._commentMetaData(metadata, level) : null; - this._postComment(metadata ? [colorizedCommentMessage, commentMetaData].join("\n") : colorizedCommentMessage); - } - - const toSupabase = { log: logMessage, level, metadata } as LogInsert; - - this._save(toSupabase); - - return new LogReturn( - { - raw: logMessage, - diff: this._diffColorCommentMessage(type, logMessage), - type, - level, - }, - metadata - ); - } - private _addDiagnosticInformation(metadata: any) { - // this is a utility function to get the name of the function that called the log - // I have mixed feelings on this because it manipulates metadata later possibly without the developer understanding why and where, - // but seems useful for the metadata parser to understand where the comment originated from - - if (!metadata) { - metadata = {}; - } - if (typeof metadata == "string" || typeof metadata == "number") { - // TODO: think i need to support every data type - metadata = { message: metadata }; - } - - const stackLines = new Error().stack?.split("\n") || []; - if (stackLines.length > 3) { - const callerLine = stackLines[3]; // .replace(process.cwd(), ""); - const match = callerLine.match(/at (\S+)/); - if (match) { - metadata.caller = match[1]; - } - } - - const gitCommit = COMMIT_HASH?.substring(0, 7) ?? null; - metadata.revision = gitCommit; - - return metadata; - } - - public ok(log: string, metadata?: any, postComment?: boolean): LogReturn | null { - metadata = this._addDiagnosticInformation(metadata); - return this._log({ - level: LogLevel.INFO, - consoleLog: Logs.console.ok, - logMessage: log, - metadata, - postComment, - type: "ok", - }); - } - - public info(log: string, metadata?: any, postComment?: boolean): LogReturn | null { - metadata = this._addDiagnosticInformation(metadata); - return this._log({ - level: LogLevel.INFO, - consoleLog: Logs.console.info, - logMessage: log, - metadata, - postComment, - type: "info", - }); - } - - public error(log: string, metadata?: any, postComment?: boolean): LogReturn | null { - metadata = this._addDiagnosticInformation(metadata); - return this._log({ - level: LogLevel.ERROR, - consoleLog: Logs.console.error, - logMessage: log, - metadata, - postComment, - type: "error", - }); - } - - public debug(log: string, metadata?: any, postComment?: boolean): LogReturn | null { - metadata = this._addDiagnosticInformation(metadata); - return this._log({ - level: LogLevel.DEBUG, - consoleLog: Logs.console.debug, - logMessage: log, - metadata, - postComment, - type: "debug", - }); - } - - public fatal(log: string, metadata?: any, postComment?: boolean): LogReturn | null { - if (!metadata) { - metadata = Logs.convertErrorsIntoObjects(new Error(log)); - const stack = metadata.stack as string[]; - stack.splice(1, 1); - metadata.stack = stack; - } - if (metadata instanceof Error) { - metadata = Logs.convertErrorsIntoObjects(metadata); - const stack = metadata.stack as string[]; - stack.splice(1, 1); - metadata.stack = stack; - } - - metadata = this._addDiagnosticInformation(metadata); - return this._log({ - level: LogLevel.FATAL, - consoleLog: Logs.console.fatal, - logMessage: log, - metadata, - postComment, - type: "fatal", - }); - } - - verbose(log: string, metadata?: any, postComment?: boolean): LogReturn | null { - metadata = this._addDiagnosticInformation(metadata); - return this._log({ - level: LogLevel.VERBOSE, - consoleLog: Logs.console.verbose, - logMessage: log, - metadata, - postComment, - type: "verbose", - }); - } - - constructor(supabase: SupabaseClient, retryLimit: number, logLevel: LogLevel, context: ProbotContext | null) { - this._supabase = supabase; - this._context = context; - this._retryLimit = retryLimit; - this._maxLevel = this._getNumericLevel(logLevel); - Logs.console = new PrettyLogs(); - } - - private async _sendLogsToSupabase(log: LogInsert) { - const { error } = await this._supabase.from("logs").insert(log); - if (error) throw Logs.console.fatal("Error logging to Supabase:", error); - } - - private async _processLogs(log: LogInsert) { - try { - await this._sendLogsToSupabase(log); - } catch (error) { - Logs.console.fatal("Error sending log, retrying:", error); - return this._retryLimit > 0 ? await this._retryLog(log) : null; - } - } - - private async _retryLog(log: LogInsert, retryCount = 0) { - if (retryCount >= this._retryLimit) { - Logs.console.fatal("Max retry limit reached for log:", log); - return; - } - - await new Promise((resolve) => setTimeout(resolve, this._retryDelay)); - - try { - await this._sendLogsToSupabase(log); - } catch (error) { - Logs.console.fatal("Error sending log (after retry):", error); - await this._retryLog(log, retryCount + 1); - } - } - - private async _processLogQueue() { - while (this._queue.length > 0) { - const log = this._queue.shift(); - if (!log) { - continue; - } - await this._processLogs(log); - } - } - - private async _throttle() { - if (this._throttleCount >= this._concurrency) { - return; - } - - this._throttleCount++; - try { - await this._processLogQueue(); - } finally { - this._throttleCount--; - if (this._queue.length > 0) { - await this._throttle(); - } - } - } - - private async _addToQueue(log: LogInsert) { - this._queue.push(log); - if (this._throttleCount < this._concurrency) { - await this._throttle(); - } - } - - private _save(logInsert: LogInsert) { - this._addToQueue(logInsert) - .then(() => void 0) - .catch(() => Logs.console.fatal("Error adding logs to queue")); - - // Logs.console.ok(logInsert.log, logInsert); - } - - static _commentMetaData(metadata: any, level: LogLevel) { - Runtime.getState().logger.debug("the main place that metadata is being serialized as an html comment"); - const prettySerialized = JSON.stringify(metadata, null, 2); - // first check if metadata is an error, then post it as a json comment - // otherwise post it as an html comment - if (level === LogLevel.FATAL) { - return ["```json", prettySerialized, "```"].join("\n"); - } else { - return [""].join("\n"); - } - } - - private _diffColorCommentMessage(type: string, message: string) { - const diffPrefix = { - fatal: "-", // - text in red - ok: "+", // + text in green - error: "!", // ! text in orange - // info: "#", // # text in gray - // debug: "@@@@",// @@ text in purple (and bold)@@ - // error: null, - // warn: null, - // info: null, - // verbose: "#", - // debug: "#", - }; - const selected = diffPrefix[type as keyof typeof diffPrefix]; - - if (selected) { - message = message - .trim() // Remove leading and trailing whitespace - .split("\n") - .map((line) => `${selected} ${line}`) - .join("\n"); - } else if (type === "debug") { - // debug has special formatting - message = message - .split("\n") - .map((line) => `@@ ${line} @@`) - .join("\n"); // debug: "@@@@", - } else { - // default to gray - message = message - .split("\n") - .map((line) => `# ${line}`) - .join("\n"); - } - - const diffHeader = "```diff"; - const diffFooter = "```"; - - return [diffHeader, message, diffFooter].join("\n"); - } - - private _postComment(message: string) { - // post on issue - if (!this._context) return; - this._context.octokit.issues - .createComment({ - owner: this._context.issue().owner, - repo: this._context.issue().repo, - issue_number: this._context.issue().issue_number, - body: message, - }) - // .then((x) => console.trace(x)) - .catch((x) => console.trace(x)); - } - - private _getNumericLevel(level: LogLevel) { - switch (level) { - case LogLevel.FATAL: - return 0; - case LogLevel.ERROR: - return 1; - case LogLevel.INFO: - return 2; - case LogLevel.VERBOSE: - return 4; - case LogLevel.DEBUG: - return 5; - default: - return -1; // Invalid level - } - } - static convertErrorsIntoObjects(obj: any): any { - // this is a utility function to render native errors in the console, the database, and on GitHub. - if (obj instanceof Error) { - return { - message: obj.message, - name: obj.name, - stack: obj.stack ? obj.stack.split("\n") : null, - }; - } else if (typeof obj === "object" && obj !== null) { - const keys = Object.keys(obj); - keys.forEach((key) => { - obj[key] = this.convertErrorsIntoObjects(obj[key]); - }); - } - return obj; - } -} diff --git a/src/adapters/supabase/helpers/tables/settlement.ts b/src/adapters/supabase/helpers/tables/settlement.ts index 85e43545e..9e7abf8f2 100644 --- a/src/adapters/supabase/helpers/tables/settlement.ts +++ b/src/adapters/supabase/helpers/tables/settlement.ts @@ -76,7 +76,7 @@ export class Settlement extends Super { const { data: debitInsertData, error: debitError } = await this.supabase .from("debits") .insert(debitData) - .select("*") + .select() .maybeSingle(); if (debitError) throw new Error(debitError.message); @@ -93,6 +93,7 @@ export class Settlement extends Super { const { data: settlementInsertData, error: settlementError } = await this.supabase .from("settlements") .insert(settlementData) + .select() .maybeSingle(); if (settlementError) throw new Error(settlementError.message); @@ -115,7 +116,7 @@ export class Settlement extends Super { const { data: creditInsertData, error: creditError } = await this.supabase .from("credits") .insert(creditData) - .select("*") + .select() .maybeSingle(); if (creditError) throw new Error(creditError.message); @@ -137,7 +138,7 @@ export class Settlement extends Super { beneficiary_id: userId, }; - const permitResult = await this.supabase.from("permits").insert(permitData).select("*").maybeSingle(); + const permitResult = await this.supabase.from("permits").insert(permitData).select().maybeSingle(); if (permitResult.error) throw new Error(permitResult.error.message); if (!permitResult.data) throw new Error("Permit not inserted"); @@ -165,6 +166,7 @@ export class Settlement extends Super { const { data: settlementInsertData, error: settlementError } = await this.supabase .from("settlements") .insert(settlementData) + .select() .maybeSingle(); if (settlementError) throw new Error(settlementError.message); diff --git a/src/adapters/supabase/helpers/tables/wallet.ts b/src/adapters/supabase/helpers/tables/wallet.ts index 3d1a798e0..804eae518 100644 --- a/src/adapters/supabase/helpers/tables/wallet.ts +++ b/src/adapters/supabase/helpers/tables/wallet.ts @@ -32,17 +32,16 @@ export class Wallet extends Super { | ProbotContext<"issue_comment.edited">["payload"]; const userData = await this._getUserData(payload); - const registeredWalletData = await this._getRegisteredWalletData(userData); - const locationMetaData = this._getLocationMetaData(payload); - if (!registeredWalletData) { + if (!userData.wallet_id) { await this._registerNewWallet({ address, locationMetaData, payload, }); } else { + const registeredWalletData = await this._getRegisteredWalletData(userData); await this._updateExistingWallet({ address, locationMetaData, @@ -87,12 +86,12 @@ export class Wallet extends Super { const { data: locationData, error: locationError } = (await this.supabase .from("locations") .insert(locationMetaData) + .select() .single()) as { data: LocationRow; error: PostgrestError | null }; if (locationError) { throw new Error(locationError.message); } - // Get the ID of the inserted location const locationId = locationData.id; @@ -100,6 +99,7 @@ export class Wallet extends Super { const { data: userData, error: userError } = await this.supabase .from("users") .insert([{ id: user.id, location_id: locationId /* other fields if necessary */ }]) + .select() .single(); if (userError) { @@ -167,6 +167,7 @@ export class Wallet extends Super { const { data: walletInsertData, error: walletInsertError } = await this.supabase .from("wallets") .insert(newWallet) + .select() .single(); if (walletInsertError) throw walletInsertError; diff --git a/src/bindings/bot-runtime.ts b/src/bindings/bot-runtime.ts index 650fc0925..8fadaba14 100644 --- a/src/bindings/bot-runtime.ts +++ b/src/bindings/bot-runtime.ts @@ -1,5 +1,5 @@ import { createAdapters } from "../adapters/adapters"; -import { Logs } from "../adapters/supabase/helpers/tables/logs"; +import { Logs } from "ubiquibot-logger"; class Runtime { private static _instance: Runtime; diff --git a/src/bindings/event.ts b/src/bindings/event.ts index 603bb9a6c..92231a85c 100644 --- a/src/bindings/event.ts +++ b/src/bindings/event.ts @@ -1,8 +1,8 @@ import OpenAI from "openai"; import { Context as ProbotContext } from "probot"; +import { LogReturn, Logs } from "ubiquibot-logger"; import zlib from "zlib"; import { createAdapters, supabaseClient } from "../adapters/adapters"; -import { LogReturn, Logs } from "../adapters/supabase/helpers/tables/logs"; import { processors, wildcardProcessors } from "../handlers/processors"; import { validateConfigChange } from "../handlers/push/push"; import structuredMetadata from "../handlers/shared/structured-metadata"; @@ -16,7 +16,7 @@ import { MainActionHandler, PostActionHandler, PreActionHandler, - WildCardHandler, + WildCardHandler } from "../types/handlers"; import { GitHubEvent, GitHubPayload, payloadSchema } from "../types/payload"; import { ajv } from "../utils/ajv"; diff --git a/src/handlers/pricing/pricing-label.ts b/src/handlers/pricing/pricing-label.ts index ee10ec43f..a388821f5 100644 --- a/src/handlers/pricing/pricing-label.ts +++ b/src/handlers/pricing/pricing-label.ts @@ -142,6 +142,7 @@ async function addPriceLabelToIssue(context: Context, targetPriceLabel: string) } export async function labelExists(context: Context, name: string): Promise { +<<<<<<< HEAD const payload = context.event.payload as GitHubPayload; const res = await context.event.octokit.rest.issues.getLabel({ owner: payload.repository.owner.login, @@ -149,6 +150,19 @@ export async function labelExists(context: Context, name: string): Promise>>>>>> a7034915869f1c0547427bbe081402908d7977d9 } async function getAllLabeledEvents(context: Context) { diff --git a/src/handlers/wildcard/unassign/unassign.ts b/src/handlers/wildcard/unassign/unassign.ts index 0f493c018..87806726d 100644 --- a/src/handlers/wildcard/unassign/unassign.ts +++ b/src/handlers/wildcard/unassign/unassign.ts @@ -1,3 +1,407 @@ import { RestEndpointMethodTypes } from "@octokit/rest"; +<<<<<<< HEAD export type IssuesListEventsResponseData = RestEndpointMethodTypes["issues"]["listEvents"]["response"]["data"]; +======= +type IssuesListEventsResponseData = RestEndpointMethodTypes["issues"]["listEvents"]["response"]["data"]; +// type Commit[] = Commit[]; // RestEndpointMethodTypes["pulls"]["listCommits"]["response"]["data"]; + +export async function checkTasksToUnassign(context: Context) { + const logger = context.logger; + const issuesAndPullsOpened = await listAllIssuesAndPullsForRepo(context, IssueType.OPEN); + const assignedIssues = issuesAndPullsOpened.filter((issue) => issue.assignee); + + const tasksToUnassign = await Promise.all( + assignedIssues.map(async (assignedIssue: Issue) => checkTaskToUnassign(context, assignedIssue)) + ); + logger.ok("Checked all the tasks to unassign", { + tasksToUnassign: tasksToUnassign.filter(Boolean).map((task) => task?.metadata), + }); +} + +async function checkTaskToUnassign(context: Context, assignedIssue: Issue) { + const logger = context.logger; + const payload = context.event.payload as Payload; + const { + timers: { taskDisqualifyDuration, taskFollowUpDuration }, + } = context.config; + + logger.info("Checking for neglected tasks", { issueNumber: assignedIssue.number }); + + if (!assignedIssue.assignees) { + throw logger.error("No assignees found when there are supposed to be assignees.", { + issueNumber: assignedIssue.number, + }); + } + const assignees = assignedIssue.assignees.filter((item): item is User => item !== null); + + const assigneeLoginsOnly = assignees.map((assignee) => assignee.login); + + const login = payload.repository.owner.login; + const name = payload.repository.name; + const number = assignedIssue.number; + + // DONE: check events - e.g. https://api.github.com/repos/ubiquity/ubiquibot/issues/644/events?per_page=100 + + const { assigneeEvents, assigneeCommits } = await aggregateAssigneeActivity({ + context, + login, + name, + number, + assignees: assigneeLoginsOnly, + }); + + // Check if the assignee did any "event activity" or commit within the timeout window + const { activeAssigneesInDisqualifyDuration, activeAssigneesInFollowUpDuration } = getActiveAssignees( + assigneeLoginsOnly, + assigneeEvents, + taskDisqualifyDuration, + assigneeCommits, + taskFollowUpDuration + ); + + // assigneeEvents + + const assignEventsOfAssignee = assigneeEvents.filter((event) => { + // check if the event is an assign event and if the assignee is the same as the assignee we're checking + if (event.event == "assigned") { + const assignedEvent = event as AssignedEvent; + return assignedEvent.assignee.login === login; + } + }); + let latestAssignEvent; + + if (assignEventsOfAssignee.length > 0) { + latestAssignEvent = assignEventsOfAssignee.reduce((latestEvent, currentEvent) => { + const latestEventTime = new Date(latestEvent.created_at).getTime(); + const currentEventTime = new Date(currentEvent.created_at).getTime(); + return currentEventTime > latestEventTime ? currentEvent : latestEvent; + }, assignEventsOfAssignee[0]); + } else { + // Handle the case where there are no assign events + // This could be setting latestAssignEvent to a default value or throwing an error + throw logger.debug("No assign events found when there are supposed to be assign events.", { + issueNumber: assignedIssue.number, + }); + } + + const latestAssignEventTime = new Date(latestAssignEvent.created_at).getTime(); + const now = Date.now(); + + const assigneesWithinGracePeriod = assignees.filter(() => now - latestAssignEventTime < taskDisqualifyDuration); + + const assigneesOutsideGracePeriod = assignees.filter((assignee) => !assigneesWithinGracePeriod.includes(assignee)); + + const disqualifiedAssignees = await disqualifyIdleAssignees(context, { + assignees: assigneesOutsideGracePeriod.map((assignee) => assignee.login), + activeAssigneesInDisqualifyDuration, + login, + name, + number, + }); + + // DONE: follow up with those who are in `assignees` and not inside of `disqualifiedAssignees` or `activeAssigneesInFollowUpDuration` + await followUpWithTheRest(context, { + assignees: assigneesOutsideGracePeriod.map((assignee) => assignee.login), + disqualifiedAssignees, + activeAssigneesInFollowUpDuration, + login, + name, + number, + taskDisqualifyDuration, + }); + + return logger.ok("Checked task to unassign", { + issueNumber: assignedIssue.number, + disqualifiedAssignees, + }); +} + +async function followUpWithTheRest( + context: Context, + { + assignees, + disqualifiedAssignees, + activeAssigneesInFollowUpDuration, + login, + name, + number, + taskDisqualifyDuration, + }: FollowUpWithTheRest +) { + const followUpAssignees = assignees.filter( + (assignee) => !disqualifiedAssignees.includes(assignee) && !activeAssigneesInFollowUpDuration.includes(assignee) + ); + + if (followUpAssignees.length > 0) { + const followUpMessage = `@${followUpAssignees.join( + ", @" + )}, this task has been idle for a while. Please provide an update.`; + + // Fetch recent comments + const hasRecentFollowUp = await checkIfFollowUpAlreadyPosted( + context, + login, + name, + number, + followUpMessage, + taskDisqualifyDuration + ); + + if (!hasRecentFollowUp) { + try { + await context.event.octokit.rest.issues.createComment({ + owner: login, + repo: name, + issue_number: number, + body: followUpMessage, + }); + context.logger.info("Followed up with idle assignees", { followUpAssignees }); + } catch (e: unknown) { + context.logger.error("Failed to follow up with idle assignees", e); + } + } + } +} + +async function checkIfFollowUpAlreadyPosted( + context: Context, + login: string, + name: string, + number: number, + followUpMessage: string, + disqualificationPeriod: number +) { + const comments = await context.event.octokit.rest.issues.listComments({ + owner: login, + repo: name, + issue_number: number, + }); + + // Get the current time + const now = new Date().getTime(); + + // Check if a similar comment has already been posted within the disqualification period + const hasRecentFollowUp = comments.data.some( + (comment) => + comment.body === followUpMessage && + comment?.user?.type === "Bot" && + now - new Date(comment.created_at).getTime() <= disqualificationPeriod + ); + return hasRecentFollowUp; +} + +async function aggregateAssigneeActivity({ context, login, name, number, assignees }: AggregateAssigneeActivity) { + const allEvents = await getAllEvents({ context, owner: login, repo: name, issueNumber: number }); + const assigneeEvents = allEvents.filter((event) => assignees.includes(event.actor.login)); // Filter all events by assignees + + // check the linked pull request and then check that pull request's commits + + const linkedPullRequests = await getLinkedPullRequests(context, { owner: login, repository: name, issue: number }); + + const allCommits = [] as Commit[]; + for (const pullRequest of linkedPullRequests) { + try { + const commits = await getAllCommitsFromPullRequest({ + context, + owner: login, + repo: name, + pullNumber: pullRequest.number, + }); + allCommits.push(...commits); + } catch (error) { + console.trace({ error }); + // return []; + } + } + + // DONE: check commits - e.g. https://api.github.com/repos/ubiquity/ubiquibot/pulls/644/commits?per_page=100 + + // Filter all commits by assignees + const assigneeCommits = allCommits.filter((commit) => { + const name = commit.author?.login || commit.commit.committer?.name; + if (!name) { + return false; + } + assignees.includes(name); + }); + return { assigneeEvents, assigneeCommits }; +} + +async function disqualifyIdleAssignees( + context: Context, + { assignees, activeAssigneesInDisqualifyDuration, login, name, number }: DisqualifyIdleAssignees +) { + const idleAssignees = assignees.filter((assignee) => !activeAssigneesInDisqualifyDuration.includes(assignee)); + + if (idleAssignees.length > 0) { + try { + await context.event.octokit.rest.issues.removeAssignees({ + owner: login, + repo: name, + issue_number: number, + assignees: idleAssignees, + }); + context.logger.info("Unassigned idle assignees", { idleAssignees }); + } catch (e: unknown) { + context.logger.error("Failed to unassign idle assignees", e); + } + } + return idleAssignees; +} + +function getActiveAssignees( + assignees: string[], + assigneeEvents: IssuesListEventsResponseData, + taskDisqualifyDuration: number, + assigneeCommits: Commit[], + taskFollowUpDuration: number +) { + const activeAssigneesInDisqualifyDuration = getActiveAssigneesInDisqualifyDuration( + assignees, + assigneeEvents, + taskDisqualifyDuration, + assigneeCommits + ); + + const activeAssigneesInFollowUpDuration = getActiveAssigneesInFollowUpDuration( + assignees, + assigneeEvents, + taskFollowUpDuration, + assigneeCommits, + taskDisqualifyDuration + ); + + return { + activeAssigneesInDisqualifyDuration, + activeAssigneesInFollowUpDuration, + }; +} + +function getActiveAssigneesInFollowUpDuration( + assignees: string[], + assigneeEvents: IssuesListEventsResponseData, + taskFollowUpDuration: number, + assigneeCommits: Commit[], + taskDisqualifyDuration: number +) { + return assignees.filter(() => { + const assigneeEventsWithinDuration = assigneeEvents.filter( + (event) => new Date().getTime() - new Date(event.created_at).getTime() <= taskFollowUpDuration + ); + const assigneeCommitsWithinDuration = assigneeCommits.filter((commit) => { + const date = commit.commit.author?.date || commit.commit.committer?.date || ""; + return date && new Date().getTime() - new Date(date).getTime() <= taskDisqualifyDuration; + }); + return assigneeEventsWithinDuration.length === 0 && assigneeCommitsWithinDuration.length === 0; + }); +} + +function getActiveAssigneesInDisqualifyDuration( + assignees: string[], + assigneeEvents: IssuesListEventsResponseData, + taskDisqualifyDuration: number, + assigneeCommits: Commit[] +) { + return assignees.filter(() => { + const assigneeEventsWithinDuration = assigneeEvents.filter( + (event) => new Date().getTime() - new Date(event.created_at).getTime() <= taskDisqualifyDuration + ); + + const assigneeCommitsWithinDuration = assigneeCommits.filter((commit) => { + const date = commit.commit.author?.date || commit.commit.committer?.date || ""; + return date && new Date().getTime() - new Date(date).getTime() <= taskDisqualifyDuration; + }); + return assigneeEventsWithinDuration.length === 0 && assigneeCommitsWithinDuration.length === 0; + }); +} + +async function getAllEvents({ context, owner, repo, issueNumber }: GetAllEvents) { + try { + const events = (await context.octokit.paginate( + context.octokit.rest.issues.listEvents, + { + owner, + repo, + issue_number: issueNumber, + per_page: 100, + }, + (response) => response.data.filter((event) => isCorrectType(event as IssuesListEventsResponseData[0])) + )) as IssuesListEventsResponseData; + return events; + } catch (err: unknown) { + context.logger.error("Failed to fetch lists of events", err); + return []; + } +} + +async function getAllCommitsFromPullRequest({ context, owner, repo, pullNumber }: GetAllCommits) { + try { + const commits = (await context.octokit.paginate(context.octokit.pulls.listCommits, { + owner, + repo, + pull_number: pullNumber, + per_page: 100, + })) as Commit[]; + return commits; + } catch (err: unknown) { + context.logger.error("Failed to fetch lists of commits", err); + return []; + } +} + +function isCorrectType(event: IssuesListEventsResponseData[0]) { + return event && typeof event.id === "number"; +} + +interface DisqualifyIdleAssignees { + assignees: string[]; + activeAssigneesInDisqualifyDuration: string[]; + login: string; + name: string; + number: number; +} + +interface FollowUpWithTheRest { + assignees: string[]; + disqualifiedAssignees: string[]; + activeAssigneesInFollowUpDuration: string[]; + login: string; + name: string; + number: number; + taskDisqualifyDuration: number; +} + +interface AggregateAssigneeActivity { + context: Context; + login: string; + name: string; + number: number; + assignees: string[]; +} +interface GetAllEvents { + context: Context; + owner: string; + repo: string; + issueNumber: number; +} +interface GetAllCommits { + context: Context; + owner: string; + repo: string; + pullNumber: number; +} +type AssignedEvent = { + id: number; + node_id: string; + url: string; + actor: User; + event: "assigned"; + commit_id: null; + commit_url: null; + created_at: string; + assignee: User; + assigner: User; + performed_via_github_app: null; +}; +>>>>>>> a7034915869f1c0547427bbe081402908d7977d9 diff --git a/src/helpers/issue.ts b/src/helpers/issue.ts index e4216ffbb..674a7c547 100644 --- a/src/helpers/issue.ts +++ b/src/helpers/issue.ts @@ -1,4 +1,4 @@ -import { LogReturn } from "../adapters/supabase/helpers/tables/logs"; +import { LogReturn } from "ubiquibot-logger"; import { Context } from "../types/context"; import { HandlerReturnValuesNoVoid } from "../types/handlers"; import { GitHubComment } from "../types/payload"; diff --git a/src/types/configuration-types.ts b/src/types/configuration-types.ts index 20bc125b5..0ff6e633c 100644 --- a/src/types/configuration-types.ts +++ b/src/types/configuration-types.ts @@ -1,6 +1,6 @@ import { ObjectOptions, Static, StaticDecode, StringOptions, TProperties, Type as T } from "@sinclair/typebox"; import ms from "ms"; -import { LogLevel } from "../adapters/supabase/helpers/pretty-logs"; +import { LogLevel } from "ubiquibot-logger/pretty-logs"; import { userCommands } from "../handlers/comment/handlers/comment-handler-main"; import { ajv } from "../utils/ajv"; @@ -78,6 +78,7 @@ const botConfigSchema = strictObject( setLabel: T.Boolean({ default: true }), fundExternalClosedIssue: T.Boolean({ default: true }), }), + isNftRewardEnabled: T.Boolean({ default: false }), }), timers: strictObject({ diff --git a/src/types/context.ts b/src/types/context.ts index bfaf4696c..cef586ce1 100644 --- a/src/types/context.ts +++ b/src/types/context.ts @@ -1,6 +1,6 @@ import OpenAI from "openai"; import { Context as ProbotContext, ProbotOctokit } from "probot"; -import { Logs } from "../adapters/supabase/helpers/tables/logs"; +import { Logs } from "ubiquibot-logger"; import { BotConfig } from "./configuration-types"; import { GitHubPayload } from "./payload"; diff --git a/src/types/handlers.ts b/src/types/handlers.ts index a8c6cfaa8..698f47e34 100644 --- a/src/types/handlers.ts +++ b/src/types/handlers.ts @@ -1,4 +1,4 @@ -import { LogReturn } from "../adapters/supabase/helpers/tables/logs"; +import { LogReturn } from "ubiquibot-logger"; import { Context } from "./context"; export type HandlerReturnValuesNoVoid = null | string | LogReturn; diff --git a/yarn.lock b/yarn.lock index 9dd6fc010..7062072bf 100644 --- a/yarn.lock +++ b/yarn.lock @@ -15745,7 +15745,7 @@ __metadata: languageName: node linkType: hard -"pino-std-serializers@npm:*, pino-std-serializers@npm:^6.0.0": +"pino-std-serializers@npm:*, pino-std-serializers@npm:^6.0.0, pino-std-serializers@npm:^6.2.2": version: 6.2.2 resolution: "pino-std-serializers@npm:6.2.2" checksum: 8f1c7f0f0d8f91e6c6b5b2a6bfb48f06441abeb85f1c2288319f736f9c6d814fbeebe928d2314efc2ba6018fa7db9357a105eca9fc99fc1f28945a8a8b28d3d5 @@ -18669,6 +18669,18 @@ __metadata: languageName: node linkType: hard +"ubiquibot-logger@npm:^0.3.5": + version: 0.3.5 + resolution: "ubiquibot-logger@npm:0.3.5" + dependencies: + "@supabase/supabase-js": "npm:^2.4.0" + dotenv: "npm:^16.3.1" + pino-std-serializers: "npm:^6.2.2" + probot: "npm:^12.2.4" + checksum: a2fc2e1082dca4281016e7fdfe65b0f13a82c754350137ef447509cb41e4297efd2176f366ea398854833f1cabcd1d33f069a0afed85fa84377c4fab34b4838a + languageName: node + linkType: hard + "ubiquibot@workspace:.": version: 0.0.0-use.local resolution: "ubiquibot@workspace:." @@ -18725,6 +18737,7 @@ __metadata: ts-jest: "npm:^29.1.1" tsx: "npm:^3.12.7" typescript: "npm:^4.9.5" + ubiquibot-logger: "npm:^0.3.5" yaml: "npm:^2.2.2" zlib: "npm:^1.0.5" languageName: unknown