diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 82c537bb4..8b1378917 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ -* @0xcodercrane \ No newline at end of file + diff --git a/.github/ubiquibot-config.yml b/.github/ubiquibot-config.yml index 48a9f91c4..f79f61652 100644 --- a/.github/ubiquibot-config.yml +++ b/.github/ubiquibot-config.yml @@ -1,6 +1,6 @@ --- -chain-id: 100 -base-multiplier: 1500 +evm-network-id: 100 +price-multiplier: 1.5 time-labels: - name: "Time: <1 Hour" weight: 0.125 @@ -28,6 +28,12 @@ priority-labels: weight: 4 - name: "Priority: 4 (Emergency)" weight: 5 +default-labels: + - "Time: <1 Hour" + - "Priority: 0 (Normal)" + - "Test" auto-pay-mode: true -analytics-mode: true +comment-incentives: true max-concurrent-bounties: 2 +promotion-comment: "\n
If you enjoy the DevPool experience, please follow Ubiquity on GitHub and star this repo to show your support. It helps a lot!
" +register-wallet-with-verification: false diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8843bee39..973953d53 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -23,3 +23,26 @@ jobs: - name: Lint run: yarn lint + + run-migration: + runs-on: ubuntu-latest + needs: build + if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || github.ref == 'refs/heads/development') + env: + SUPABASE_ACCESS_TOKEN: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SUPABASE_ACCESS_TOKEN || secrets.STAGING_SUPABASE_ACCESS_TOKEN }} + SUPABASE_DB_PASSWORD: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SUPABASE_DB_PASSWORD || secrets.STAGING_SUPABASE_DB_PASSWORD }} + PROJECT_ID: ${{ github.ref == 'refs/heads/main' && secrets.PRODUCTION_SUPABASE_PROJECT_ID || secrets.STAGING_SUPABASE_PROJECT_ID }} + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - uses: supabase/setup-cli@v1 + with: + version: latest + + - name: Link Supabase project + run: supabase link --project-ref $PROJECT_ID + + - name: Run migrations + run: supabase db push diff --git a/README.md b/README.md index 5ad4a1eeb..0311933d0 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,37 @@ To test the bot, you can: 2. Add a time label, ex: `Time: <1 Day` 3. At this point the bot should add a price label. +## Configuration + +`chain-id` is ID of the EVM-compatible network that will be used for payouts. + +`base-multiplier` is a base number that will be used to calculate bounty price based on the following formula: `price = base-multiplier * time-label-weight * priority-label-weight / 10` + +`time-labels` are labels for marking the time limit of the bounty: + +- `name` is a human-readable name +- `weight` is a number that will be used to calculate the bounty price +- `value` is number of seconds that corresponds to the time limit of the bounty + +`priority-labels` are labels for marking the priority of the bounty: + +- `name` is a human-readable name +- `weight` is a number that will be used to calculate the bounty price + +`default-labels` are labels that are applied when an issue is created without any time or priority labels. + +`auto-pay-mode` can be `true` or `false` that enables or disables automatic payout of bounties when the issue is closed. + +`analytics-mode` can be `true` or `false` that enables or disables weekly analytics collection by Ubiquity. + +`incentive-mode` can be `true` or `false` that enables or disables comment incentives. These are comments in the issue by either the creator of the bounty or other users. + +`issue-creator-multiplier` is a number that defines a base multiplier for calculating incentive reward for the creator of the issue. + +`comment-element-pricing` defines how much is a part of the comment worth. For example `text: 0.1` means that any text in the comment will be multiplied by 0.1 + +`max-concurrent-bounties` is the maximum number of bounties that can be assigned to a bounty hunter at once. This excludes bounties with pending pull request reviews. + ## How to run locally 1. Create a new project at [Supabase](https://supabase.com/). Add `Project URL` and `API Key` to the `.env` file: diff --git a/src/adapters/supabase/helpers/client.ts b/src/adapters/supabase/helpers/client.ts index cfa3d68d9..335ba0e95 100644 --- a/src/adapters/supabase/helpers/client.ts +++ b/src/adapters/supabase/helpers/client.ts @@ -290,6 +290,22 @@ export const getWalletMultiplier = async (username: string): Promise => else return data?.multiplier; }; +/** + * Queries both the wallet multiplier and address in one request registered previously + * + * @param username The username you want to find an address for + * @returns The Multiplier and ERC-20 Address, returns 1 if not found + * + */ + +export const getWalletInfo = async (username: string): Promise<{ multiplier: number | null; address: string | null } | number | undefined> => { + const { supabase } = getAdapters(); + + const { data } = await supabase.from("wallets").select("multiplier, address").eq("user_name", username).single(); + if (data?.multiplier == null || data?.address == null) return 1; + else return { multiplier: data?.multiplier, address: data?.address }; +}; + export const getMultiplierReason = async (username: string): Promise => { const { supabase } = getAdapters(); const { data } = await supabase.from("wallets").select("reason").eq("user_name", username).single(); diff --git a/src/adapters/supabase/types/database.d.ts b/src/adapters/supabase/types/database.ts similarity index 68% rename from src/adapters/supabase/types/database.d.ts rename to src/adapters/supabase/types/database.ts index c727abb19..48a6256bf 100644 --- a/src/adapters/supabase/types/database.d.ts +++ b/src/adapters/supabase/types/database.ts @@ -1,4 +1,4 @@ -export type Json = string | number | boolean | null | { [key: string]: Json } | Json[]; +export type Json = string | number | boolean | null | { [key: string]: Json | undefined } | Json[]; export interface Database { graphql_public: { @@ -11,10 +11,10 @@ export interface Database { Functions: { graphql: { Args: { - operationName: string; - query: string; - variables: Json; - extensions: Json; + operationName?: string; + query?: string; + variables?: Json; + extensions?: Json; }; Returns: Json; }; @@ -22,9 +22,45 @@ export interface Database { Enums: { [_ in never]: never; }; + CompositeTypes: { + [_ in never]: never; + }; }; public: { Tables: { + access: { + Row: { + created_at: string | null; + multiplier_access: boolean | null; + price_access: boolean | null; + priority_access: boolean | null; + repository: string | null; + time_access: boolean | null; + updated_at: string | null; + user_name: string; + }; + Insert: { + created_at?: string | null; + multiplier_access?: boolean | null; + price_access?: boolean | null; + priority_access?: boolean | null; + repository?: string | null; + time_access?: boolean | null; + updated_at?: string | null; + user_name: string; + }; + Update: { + created_at?: string | null; + multiplier_access?: boolean | null; + price_access?: boolean | null; + priority_access?: boolean | null; + repository?: string | null; + time_access?: boolean | null; + updated_at?: string | null; + user_name?: string; + }; + Relationships: []; + }; issues: { Row: { assignees: string[] | null; @@ -86,6 +122,7 @@ export interface Database { txhash?: string[] | null; updated_at?: string | null; }; + Relationships: []; }; users: { Row: { @@ -154,26 +191,49 @@ export interface Database { user_type?: string | null; wallet_address?: string | null; }; + Relationships: []; }; wallets: { Row: { created_at: string | null; + multiplier: number | null; + reason: string | null; updated_at: string | null; user_name: string; wallet_address: string | null; }; Insert: { created_at?: string | null; + multiplier?: number | null; + reason?: string | null; updated_at?: string | null; user_name: string; wallet_address?: string | null; }; Update: { created_at?: string | null; + multiplier?: number | null; + reason?: string | null; updated_at?: string | null; user_name?: string; wallet_address?: string | null; }; + Relationships: []; + }; + weekly: { + Row: { + created_at: string | null; + last_time: string | null; + }; + Insert: { + created_at?: string | null; + last_time?: string | null; + }; + Update: { + created_at?: string | null; + last_time?: string | null; + }; + Relationships: []; }; }; Views: { @@ -185,12 +245,18 @@ export interface Database { Enums: { issue_status: "READY_TO_START" | "IN_PROGRESS" | "IN_REVIEW" | "DONE"; }; + CompositeTypes: { + [_ in never]: never; + }; }; storage: { Tables: { buckets: { Row: { + allowed_mime_types: string[] | null; + avif_autodetection: boolean | null; created_at: string | null; + file_size_limit: number | null; id: string; name: string; owner: string | null; @@ -198,7 +264,10 @@ export interface Database { updated_at: string | null; }; Insert: { + allowed_mime_types?: string[] | null; + avif_autodetection?: boolean | null; created_at?: string | null; + file_size_limit?: number | null; id: string; name: string; owner?: string | null; @@ -206,13 +275,24 @@ export interface Database { updated_at?: string | null; }; Update: { + allowed_mime_types?: string[] | null; + avif_autodetection?: boolean | null; created_at?: string | null; + file_size_limit?: number | null; id?: string; name?: string; owner?: string | null; public?: boolean | null; updated_at?: string | null; }; + Relationships: [ + { + foreignKeyName: "buckets_owner_fkey"; + columns: ["owner"]; + referencedRelation: "users"; + referencedColumns: ["id"]; + } + ]; }; migrations: { Row: { @@ -233,6 +313,7 @@ export interface Database { id?: number; name?: string; }; + Relationships: []; }; objects: { Row: { @@ -245,6 +326,7 @@ export interface Database { owner: string | null; path_tokens: string[] | null; updated_at: string | null; + version: string | null; }; Insert: { bucket_id?: string | null; @@ -256,6 +338,7 @@ export interface Database { owner?: string | null; path_tokens?: string[] | null; updated_at?: string | null; + version?: string | null; }; Update: { bucket_id?: string | null; @@ -267,39 +350,72 @@ export interface Database { owner?: string | null; path_tokens?: string[] | null; updated_at?: string | null; + version?: string | null; }; + Relationships: [ + { + foreignKeyName: "objects_bucketId_fkey"; + columns: ["bucket_id"]; + referencedRelation: "buckets"; + referencedColumns: ["id"]; + }, + { + foreignKeyName: "objects_owner_fkey"; + columns: ["owner"]; + referencedRelation: "users"; + referencedColumns: ["id"]; + } + ]; }; }; Views: { [_ in never]: never; }; Functions: { + can_insert_object: { + Args: { + bucketid: string; + name: string; + owner: string; + metadata: Json; + }; + Returns: undefined; + }; extension: { - Args: { name: string }; + Args: { + name: string; + }; Returns: string; }; filename: { - Args: { name: string }; + Args: { + name: string; + }; Returns: string; }; foldername: { - Args: { name: string }; - Returns: string[]; + Args: { + name: string; + }; + Returns: unknown; }; get_size_by_bucket: { Args: Record; - Returns: { size: number; bucket_id: string }[]; + Returns: { + size: number; + bucket_id: string; + }[]; }; search: { Args: { prefix: string; bucketname: string; - limits: number; - levels: number; - offsets: number; - search: string; - sortcolumn: string; - sortorder: string; + limits?: number; + levels?: number; + offsets?: number; + search?: string; + sortcolumn?: string; + sortorder?: string; }; Returns: { name: string; @@ -314,5 +430,8 @@ export interface Database { Enums: { [_ in never]: never; }; + CompositeTypes: { + [_ in never]: never; + }; }; } diff --git a/src/adapters/supabase/types/index.ts b/src/adapters/supabase/types/index.ts index f211e1c1a..c30cd664d 100644 --- a/src/adapters/supabase/types/index.ts +++ b/src/adapters/supabase/types/index.ts @@ -1 +1 @@ -export * from "./database.d"; +export * from "./database"; diff --git a/src/bindings/config.ts b/src/bindings/config.ts index 05d7b64c9..d354c9785 100644 --- a/src/bindings/config.ts +++ b/src/bindings/config.ts @@ -2,7 +2,7 @@ import ms from "ms"; import { BotConfig, BotConfigSchema } from "../types"; import { DEFAULT_BOT_DELAY, DEFAULT_DISQUALIFY_TIME, DEFAULT_FOLLOWUP_TIME, DEFAULT_PERMIT_BASE_URL } from "../configs"; -import { getPayoutConfigByChainId } from "../helpers"; +import { getPayoutConfigByNetworkId } from "../helpers"; import { ajv } from "../utils"; import { Context } from "probot"; import { getScalarKey, getWideConfig } from "../utils/private"; @@ -15,15 +15,18 @@ export const loadConfig = async (context: Context): Promise => { priorityLabels, commentElementPricing, autoPayMode, - analyticsMode, + disableAnalytics, bountyHunterMax, incentiveMode, - chainId, + networkId, issueCreatorMultiplier, + defaultLabels, + promotionComment, + registerWalletWithVerification, } = await getWideConfig(context); const publicKey = await getScalarKey(process.env.X25519_PRIVATE_KEY); - const { rpc, paymentToken } = getPayoutConfigByChainId(chainId); + const { rpc, paymentToken } = getPayoutConfigByNetworkId(networkId); const botConfig: BotConfig = { log: { @@ -36,9 +39,13 @@ export const loadConfig = async (context: Context): Promise => { timeLabels, priorityLabels, commentElementPricing, + defaultLabels, + }, + comments: { + promotionComment: promotionComment, }, payout: { - chainId: chainId, + networkId: networkId, rpc: rpc, privateKey: privateKey, paymentToken: paymentToken, @@ -58,7 +65,7 @@ export const loadConfig = async (context: Context): Promise => { }, mode: { autoPayMode: autoPayMode, - analyticsMode: analyticsMode, + disableAnalytics: disableAnalytics, incentiveMode: incentiveMode, }, assign: { @@ -68,6 +75,9 @@ export const loadConfig = async (context: Context): Promise => { privateKey: process.env.X25519_PRIVATE_KEY ?? "", publicKey: publicKey ?? "", }, + wallet: { + registerWalletWithVerification: registerWalletWithVerification, + }, }; if (botConfig.log.ingestionKey == "") { diff --git a/src/bindings/event.ts b/src/bindings/event.ts index f85cc680d..77518c99c 100644 --- a/src/bindings/event.ts +++ b/src/bindings/event.ts @@ -50,6 +50,7 @@ export const bindEvents = async (context: Context): Promise => { unassign: botConfig.unassign, mode: botConfig.mode, log: botConfig.log, + wallet: botConfig.wallet, })}` ); const allowedEvents = Object.values(GithubEvent) as string[]; diff --git a/src/configs/shared.ts b/src/configs/shared.ts index 65d499402..2d817b7a0 100644 --- a/src/configs/shared.ts +++ b/src/configs/shared.ts @@ -1,5 +1,3 @@ -import { generateHelpMenu } from "../handlers"; - export const COLORS = { default: "ededed", price: "1f883d", @@ -7,6 +5,7 @@ export const COLORS = { export const DEFAULT_BOT_DELAY = 100; // 100ms export const DEFAULT_TIME_RANGE_FOR_MAX_ISSUE = 24; export const DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED = true; +export const ASSIGN_COMMAND_ENABLED = true; /** * ms('2 days') // 172800000 * ms('1d') // 86400000 @@ -24,7 +23,6 @@ export const DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED = true; export const DEFAULT_FOLLOWUP_TIME = "4 days"; // 4 days export const DEFAULT_DISQUALIFY_TIME = "7 days"; // 7 days -export const DEFAULT_CHAIN_ID = 1; // ethereum +export const DEFAULT_NETWORK_ID = 1; // ethereum export const DEFAULT_RPC_ENDPOINT = "https://rpc-bot.ubq.fi/v1/mainnet"; export const DEFAULT_PERMIT_BASE_URL = "https://pay.ubq.fi"; -export const COMMAND_INSTRUCTIONS = generateHelpMenu(); diff --git a/src/configs/strings.ts b/src/configs/strings.ts index ef96a0f2b..9bc705a20 100644 --- a/src/configs/strings.ts +++ b/src/configs/strings.ts @@ -1,4 +1,5 @@ export const GLOBAL_STRINGS = { unassignComment: "Releasing the bounty back to dev pool because the allocated duration already ended!", askUpdate: "Do you have any updates", + assignCommandDisabledComment: "The `/assign` command is disabled for this repository.", }; diff --git a/src/handlers/assign/action.ts b/src/handlers/assign/action.ts index 951ad3314..bd71d65c3 100644 --- a/src/handlers/assign/action.ts +++ b/src/handlers/assign/action.ts @@ -60,7 +60,7 @@ export const commentWithAssignMessage = async (): Promise => { const curDate = new Date(); const curDateInMillisecs = curDate.getTime(); const endDate = new Date(curDateInMillisecs + duration * 1000); - const commit_msg = `${flattened_assignees} ${deadLinePrefix} ${endDate.toUTCString()}`; + const commit_msg = `${flattened_assignees} ${deadLinePrefix} ${endDate.toUTCString().replace("GMT", "UTC")}`; logger.debug(`Creating an issue comment, commit_msg: ${commit_msg}`); await addCommentToIssue(commit_msg, payload.issue?.number); diff --git a/src/handlers/assign/auto.ts b/src/handlers/assign/auto.ts index bc11cc48e..a065327f4 100644 --- a/src/handlers/assign/auto.ts +++ b/src/handlers/assign/auto.ts @@ -1,5 +1,5 @@ import { getBotContext, getLogger } from "../../bindings"; -import { addAssignees, getIssueByNumber, getPullRequests } from "../../helpers"; +import { addAssignees, getIssueByNumber, getPullByNumber, getPullRequests } from "../../helpers"; import { gitLinkedIssueParser } from "../../helpers/parser"; import { Payload } from "../../types"; @@ -29,6 +29,14 @@ export const checkPullRequests = async () => { continue; } + const connectedPull = await getPullByNumber(context, pull.number); + + // Newly created PULL (draft or direct) pull does have same `created_at` and `updated_at`. + if (connectedPull?.created_at !== connectedPull?.updated_at) { + logger.debug("It's an updated Pull Request, reverting"); + continue; + } + const linkedIssueNumber = pullRequestLinked.substring(pullRequestLinked.lastIndexOf("/") + 1); // Check if the pull request opener is assigned to the issue diff --git a/src/handlers/comment/action.ts b/src/handlers/comment/action.ts index faa7a24ae..1857b7851 100644 --- a/src/handlers/comment/action.ts +++ b/src/handlers/comment/action.ts @@ -16,15 +16,16 @@ export const handleComment = async (): Promise => { } const body = comment.body; - const commands = commentParser(body); + const commentedCommands = commentParser(body); - if (commands.length === 0) { + if (commentedCommands.length === 0) { await verifyFirstCheck(); return; } - for (const command of commands) { - const userCommand = userCommands.find((i) => i.id == command); + const allCommands = userCommands(); + for (const command of commentedCommands) { + const userCommand = allCommands.find((i) => i.id == command); if (userCommand) { const { handler, callback, successComment, failureComment } = userCommand; @@ -37,13 +38,13 @@ export const handleComment = async (): Promise => { try { const response = await handler(body); const callbackComment = response ?? successComment ?? ""; - if (callbackComment) await callback(issue.number, callbackComment); + if (callbackComment) await callback(issue.number, callbackComment, payload.action, payload.comment); } catch (err: unknown) { // Use failureComment for failed command if it is available if (failureComment) { - await callback(issue.number, failureComment); + await callback(issue.number, failureComment, payload.action, payload.comment); } - await callback(issue.number, `Error: ${err}`); + await callback(issue.number, `Error: ${err}`, payload.action, payload.comment); } } else { logger.info(`Skipping for a command: ${command}`); diff --git a/src/handlers/comment/commands.ts b/src/handlers/comment/commands.ts index 98c52990f..878727dcb 100644 --- a/src/handlers/comment/commands.ts +++ b/src/handlers/comment/commands.ts @@ -5,7 +5,7 @@ export enum IssueCommentCommands { WALLET = "/wallet", // register wallet address PAYOUT = "/payout", // request permit payout MULTIPLIER = "/multiplier", // set bounty multiplier (for treasury) - + QUERY = "/query", // Access Controls ALLOW = "/allow", diff --git a/src/handlers/comment/handlers/set-access.ts b/src/handlers/comment/handlers/allow.ts similarity index 93% rename from src/handlers/comment/handlers/set-access.ts rename to src/handlers/comment/handlers/allow.ts index 1c855690c..52d22e9bc 100644 --- a/src/handlers/comment/handlers/set-access.ts +++ b/src/handlers/comment/handlers/allow.ts @@ -50,6 +50,6 @@ export const setAccess = async (body: string) => { return `Updated access for @${username} successfully!\t Access: **${accessType}** for "${repo.full_name}"`; } else { logger.error("Invalid body for allow command"); - return `Invalid body for allow command`; + return `Invalid syntax for allow \n usage: '/allow set-(access type) @user true|false' \n ex-1 /allow set-multiplier @user false`; } }; diff --git a/src/handlers/comment/handlers/assign.ts b/src/handlers/comment/handlers/assign.ts index 9d3fddc64..2721171f4 100644 --- a/src/handlers/comment/handlers/assign.ts +++ b/src/handlers/comment/handlers/assign.ts @@ -1,10 +1,11 @@ -import { addAssignees, getAssignedIssues, getCommentsOfIssue, getAvailableOpenedPullRequests } from "../../../helpers"; +import { addAssignees, getAssignedIssues, getAvailableOpenedPullRequests, getAllIssueComments, addCommentToIssue } from "../../../helpers"; import { getBotConfig, getBotContext, getLogger } from "../../../bindings"; import { Payload, LabelItem, Comment, IssueType } from "../../../types"; import { deadLinePrefix } from "../../shared"; import { getWalletAddress, getWalletMultiplier, getMultiplierReason } from "../../../adapters/supabase"; import { tableComment } from "./table"; import { bountyInfo } from "../../wildcard"; +import { ASSIGN_COMMAND_ENABLED, GLOBAL_STRINGS } from "../../../configs"; export const assign = async (body: string) => { const { payload: _payload } = getBotContext(); @@ -19,18 +20,20 @@ export const assign = async (body: string) => { return "Skipping '/assign' because of no issue instance"; } - const opened_prs = await getAvailableOpenedPullRequests(payload.sender.login); - - logger.info(`Opened Pull Requests with no reviews but over 24 hours have passed: ${JSON.stringify(opened_prs)}`); + if (!ASSIGN_COMMAND_ENABLED) { + logger.info(`Ignore '/assign' command from user: ASSIGN_COMMAND_ENABLED config is set false`); + await addCommentToIssue(GLOBAL_STRINGS.assignCommandDisabledComment, issue.number); + return; + } - const assigned_issues = await getAssignedIssues(payload.sender.login); + const openedPullRequests = await getAvailableOpenedPullRequests(payload.sender.login); + logger.info(`Opened Pull Requests with approved reviews or with no reviews but over 24 hours have passed: ${JSON.stringify(openedPullRequests)}`); + const assignedIssues = await getAssignedIssues(payload.sender.login); logger.info(`Max issue allowed is ${config.assign.bountyHunterMax}`); - const issue_number = issue.number; - // check for max and enforce max - if (assigned_issues.length - opened_prs.length >= config.assign.bountyHunterMax) { + if (assignedIssues.length - openedPullRequests.length >= config.assign.bountyHunterMax) { return `Too many assigned issues, you have reached your max of ${config.assign.bountyHunterMax}`; } @@ -55,10 +58,10 @@ export const assign = async (body: string) => { const timeLabelsDefined = config.price.timeLabels; const timeLabelsAssigned: LabelItem[] = []; for (const _label of labels) { - const _label_type = typeof _label; - const _label_name = _label_type === "string" ? _label.toString() : _label_type === "object" ? _label.name : "unknown"; + const _labelType = typeof _label; + const _labelName = _labelType === "string" ? _label.toString() : _labelType === "object" ? _label.name : "unknown"; - const timeLabel = timeLabelsDefined.find((item) => item.name === _label_name); + const timeLabel = timeLabelsDefined.find((item) => item.name === _labelName); if (timeLabel) { timeLabelsAssigned.push(timeLabel); } @@ -73,54 +76,48 @@ export const assign = async (body: string) => { const targetTimeLabel = sorted[0]; const duration = targetTimeLabel.value; if (!duration) { - logger.info(`Missing configure for timelabel: ${targetTimeLabel.name}`); + logger.info(`Missing configure for time label: ${targetTimeLabel.name}`); return "Skipping `/assign` since configuration is missing for the following labels"; } - const curDate = new Date(); - const curDateInMillisecs = curDate.getTime(); - const endDate = new Date(curDateInMillisecs + duration * 1000); - const deadline_msg = endDate.toUTCString(); - - let wallet_msg, multiplier_msg, reason_msg, bounty_msg; - - const commit_msg = `@${payload.sender.login} ${deadLinePrefix} ${endDate.toUTCString()}`; + const startTime = new Date().getTime(); + const endTime = new Date(startTime + duration * 1000); + + const comment = { + deadline: endTime.toUTCString().replace("GMT", "UTC"), + wallet: (await getWalletAddress(payload.sender.login)) || "Please set your wallet address to use `/wallet 0x0000...0000`", + multiplier: "1.00", + reason: await getMultiplierReason(payload.sender.login), + bounty: `Permit generation skipped since price label is not set`, + commit: `@${payload.sender.login} ${deadLinePrefix} ${endTime.toUTCString()}`, + tips: `
Tips:
+
    +
  • Use /wallet 0x0000...0000 if you want to update your registered payment wallet address @user.
  • +
  • Be sure to open a draft pull request as soon as possible to communicate updates on your progress.
  • +
  • Be sure to provide timely updates to us when requested, or you will be automatically unassigned from the bounty.
  • +
      `, + }; if (!assignees.map((i) => i.login).includes(payload.sender.login)) { logger.info(`Adding the assignee: ${payload.sender.login}`); - // assign default bounty account to the issue - await addAssignees(issue_number, [payload.sender.login]); + await addAssignees(issue.number, [payload.sender.login]); } // double check whether the assign message has been already posted or not - logger.info(`Creating an issue comment: ${commit_msg}`); - const issue_comments = await getCommentsOfIssue(issue_number); - const comments = issue_comments.sort((a: Comment, b: Comment) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); - const latest_comment = comments.length > 0 ? comments[0].body : undefined; - if (latest_comment && commit_msg != latest_comment) { - const recipient = await getWalletAddress(payload.sender.login); - if (!recipient) { - //no wallet found - wallet_msg = "Please set your wallet address to use `/wallet 0x4FDE...BA18`"; - } else { - //wallet found - wallet_msg = recipient; - } + logger.info(`Creating an issue comment: ${comment.commit}`); + const issueComments = await getAllIssueComments(issue.number); + const comments = issueComments.sort((a: Comment, b: Comment) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); + const latestComment = comments.length > 0 ? comments[0].body : undefined; + if (latestComment && comment.commit != latestComment) { const multiplier = await getWalletMultiplier(payload.sender.login); - if (!multiplier) { - multiplier_msg = "1.00"; - } else { - multiplier_msg = multiplier.toFixed(2); + if (multiplier) { + comment.multiplier = multiplier.toFixed(2); } const issueDetailed = bountyInfo(issue); - if (!issueDetailed.priceLabel) { - bounty_msg = `Permit generation skipped since price label is not set`; - } else { - bounty_msg = (+issueDetailed.priceLabel.substring(7, issueDetailed.priceLabel.length - 4) * multiplier).toString() + " USD"; + if (issueDetailed.priceLabel) { + comment.bounty = (+issueDetailed.priceLabel.substring(7, issueDetailed.priceLabel.length - 4) * multiplier).toString() + " USD"; } - const reason = await getMultiplierReason(payload.sender.login); - reason_msg = reason ?? ""; - return tableComment(deadline_msg, wallet_msg, multiplier_msg, reason_msg, bounty_msg); + return tableComment(comment) + comment.tips; } return; }; diff --git a/src/handlers/comment/handlers/first.ts b/src/handlers/comment/handlers/first.ts index 13aa8163a..c14d50702 100644 --- a/src/handlers/comment/handlers/first.ts +++ b/src/handlers/comment/handlers/first.ts @@ -1,7 +1,7 @@ import { getBotContext, getLogger } from "../../../bindings"; -import { COMMAND_INSTRUCTIONS } from "../../../configs"; -import { addCommentToIssue } from "../../../helpers"; +import { upsertCommentToIssue } from "../../../helpers"; import { Payload } from "../../../types"; +import { generateHelpMenu } from "./help"; export const verifyFirstCheck = async (): Promise => { const context = getBotContext(); @@ -25,8 +25,8 @@ export const verifyFirstCheck = async (): Promise => { const isFirstComment = resp.data.filter((item) => item.user?.login === payload.sender.login).length === 1; if (isFirstComment) { //first_comment - const msg = `${COMMAND_INSTRUCTIONS}\n@${payload.sender.login}`; - await addCommentToIssue(msg, payload.issue.number); + const msg = `${generateHelpMenu()}\n@${payload.sender.login}`; + await upsertCommentToIssue(payload.issue.number, msg, payload.action, payload.comment); } } } catch (error: unknown) { diff --git a/src/handlers/comment/handlers/help.ts b/src/handlers/comment/handlers/help.ts index 84764c00b..9c461e84a 100644 --- a/src/handlers/comment/handlers/help.ts +++ b/src/handlers/comment/handlers/help.ts @@ -1,5 +1,6 @@ import { userCommands } from "."; import { getBotContext, getLogger } from "../../../bindings"; +import { ASSIGN_COMMAND_ENABLED } from "../../../configs"; import { IssueType, Payload } from "../../../types"; import { IssueCommentCommands } from "../commands"; @@ -28,19 +29,20 @@ export const listAvailableCommands = async (body: string) => { export const generateHelpMenu = () => { let helpMenu = "### Available commands\n```"; - - userCommands.map((command) => { + const commands = userCommands(); + commands.map((command) => { // if first command, add a new line - if (command.id === userCommands[0].id) { + if (command.id === commands[0].id) { helpMenu += `\n`; + if (!ASSIGN_COMMAND_ENABLED) return; } helpMenu += `- ${command.id}: ${command.description}`; // if not last command, add a new line (fixes too much space below) - if (command.id !== userCommands[userCommands.length - 1].id) { + if (command.id !== commands[commands.length - 1].id) { helpMenu += `\n`; } }); - helpMenu += "```"; + if (!ASSIGN_COMMAND_ENABLED) helpMenu += "```\n***_To assign yourself to an issue, please open a draft pull request that is linked to it._***"; return helpMenu; }; diff --git a/src/handlers/comment/handlers/index.ts b/src/handlers/comment/handlers/index.ts index c5fd31427..a304d1a10 100644 --- a/src/handlers/comment/handlers/index.ts +++ b/src/handlers/comment/handlers/index.ts @@ -1,5 +1,4 @@ -import { getBotConfig } from "../../../bindings"; -import { Payload, UserCommands } from "../../../types"; +import { Comment, Payload, UserCommands } from "../../../types"; import { IssueCommentCommands } from "../commands"; import { assign } from "./assign"; import { listAvailableCommands } from "./help"; @@ -7,11 +6,12 @@ import { listAvailableCommands } from "./help"; // import { payout } from "./payout"; import { unassign } from "./unassign"; import { registerWallet } from "./wallet"; -import { setAccess } from "./set-access"; +import { setAccess } from "./allow"; import { multiplier } from "./multiplier"; -import { addCommentToIssue, createLabel, addLabelToIssue } from "../../../helpers"; -import { getBotContext } from "../../../bindings"; +import { addCommentToIssue, createLabel, addLabelToIssue, getLabel, upsertCommentToIssue } from "../../../helpers"; +import { getBotConfig, getBotContext } from "../../../bindings"; import { handleIssueClosed } from "../../payout"; +import { query } from "./query"; export * from "./assign"; export * from "./wallet"; @@ -19,6 +19,7 @@ export * from "./unassign"; export * from "./payout"; export * from "./help"; export * from "./multiplier"; +export * from "./query"; /** * Parses the comment body and figure out the command name a user wants @@ -27,12 +28,19 @@ export * from "./multiplier"; * @param body - The comment body * @returns The list of command names the comment includes */ + export const commentParser = (body: string): IssueCommentCommands[] => { - // TODO: As a starting point, it may be simple but there could be cases for the comment to includes one or more commands - // We need to continuously improve to parse even complex comments. Right now, we implement it simply. - const commandList = Object.values(IssueCommentCommands) as string[]; - const result = commandList.filter((command: string) => body.startsWith(command)); - return result as IssueCommentCommands[]; + const regex = /^\/(\w+)\b/; // Regex pattern to match the command at the beginning of the body + + const matches = regex.exec(body); + if (matches) { + const command = matches[0] as IssueCommentCommands; + if (Object.values(IssueCommentCommands).includes(command)) { + return [command]; + } + } + + return []; }; /** @@ -41,15 +49,12 @@ export const commentParser = (body: string): IssueCommentCommands[] => { export const issueClosedCallback = async (): Promise => { const { payload: _payload } = getBotContext(); + const { comments } = getBotConfig(); const issue = (_payload as Payload).issue; if (!issue) return; try { const comment = await handleIssueClosed(); - if (comment) await addCommentToIssue(comment, issue.number); - await addCommentToIssue( - `If you enjoy the DevPool experience, please follow Ubiquity on GitHub and star this repo to show your support. It helps a lot!`, - issue.number - ); + if (comment) await addCommentToIssue(comment + comments.promotionComment, issue.number); } catch (err: unknown) { return await addCommentToIssue(`Error: ${err}`, issue.number); } @@ -66,16 +71,16 @@ export const issueCreatedCallback = async (): Promise => { if (!issue) return; const labels = issue.labels; try { - const timeLabelConfigs = config.price.timeLabels.sort((label1, label2) => label1.weight - label2.weight); - const priorityLabelConfigs = config.price.priorityLabels.sort((label1, label2) => label1.weight - label2.weight); const timeLabels = config.price.timeLabels.filter((item) => labels.map((i) => i.name).includes(item.name)); const priorityLabels = config.price.priorityLabels.filter((item) => labels.map((i) => i.name).includes(item.name)); - if (timeLabels.length === 0 && timeLabelConfigs.length > 0) await createLabel(timeLabelConfigs[0].name); - if (priorityLabels.length === 0 && priorityLabelConfigs.length > 0) await createLabel(priorityLabelConfigs[0].name); - await addLabelToIssue(timeLabelConfigs[0].name); - await addLabelToIssue(priorityLabelConfigs[0].name); - return; + if (timeLabels.length === 0 && priorityLabels.length === 0) { + for (const label of config.price.defaultLabels) { + const exists = await getLabel(label); + if (!exists) await createLabel(label); + await addLabelToIssue(label); + } + } } catch (err: unknown) { return await addCommentToIssue(`Error: ${err}`, issue.number); } @@ -88,52 +93,65 @@ export const issueCreatedCallback = async (): Promise => { * @param issue_number - The issue number * @param comment - Comment string */ -const commandCallback = async (issue_number: number, comment: string) => { - await addCommentToIssue(comment, issue_number); + +const commandCallback = async (issue_number: number, comment: string, action: string, reply_to?: Comment) => { + await upsertCommentToIssue(issue_number, comment, action, reply_to); }; -export const userCommands: UserCommands[] = [ - { - id: IssueCommentCommands.ASSIGN, - description: "Assign the origin sender to the issue automatically.", - handler: assign, - callback: commandCallback, - }, - { - id: IssueCommentCommands.UNASSIGN, - description: "Unassign the origin sender from the issue automatically.", - handler: unassign, - callback: commandCallback, - }, - { - handler: listAvailableCommands, - id: IssueCommentCommands.HELP, - description: "List all available commands.", - callback: commandCallback, - }, - // Commented out until Gnosis Safe is integrated (https://github.com/ubiquity/ubiquibot/issues/353) - /*{ +export const userCommands = (): UserCommands[] => { + const config = getBotConfig(); + + return [ + { + id: IssueCommentCommands.ASSIGN, + description: "Assign the origin sender to the issue automatically.", + handler: assign, + callback: commandCallback, + }, + { + id: IssueCommentCommands.UNASSIGN, + description: "Unassign the origin sender from the issue automatically.", + handler: unassign, + callback: commandCallback, + }, + { + handler: listAvailableCommands, + id: IssueCommentCommands.HELP, + description: "List all available commands.", + callback: commandCallback, + }, + // Commented out until Gnosis Safe is integrated (https://github.com/ubiquity/ubiquibot/issues/353) + /*{ id: IssueCommentCommands.PAYOUT, description: "Disable automatic payment for the issue.", handler: payout, callback: commandCallback, },*/ - { - id: IssueCommentCommands.MULTIPLIER, - description: `Set bounty multiplier (for treasury)`, - handler: multiplier, - callback: commandCallback, - }, - { - id: IssueCommentCommands.ALLOW, - description: `Set access control. (Admin Only)`, - handler: setAccess, - callback: commandCallback, - }, - { - id: IssueCommentCommands.WALLET, - description: `: Register the hunter's wallet address. \n ex1: /wallet 0xf39fd6e51aad88f6f4ce6ab8827279cfffb92266 \n ex2: /wallet vitalik.eth\n`, - handler: registerWallet, - callback: commandCallback, - }, -]; + { + id: IssueCommentCommands.QUERY, + description: `Comments the users multiplier and address`, + handler: query, + callback: commandCallback, + }, + { + id: IssueCommentCommands.MULTIPLIER, + description: `Set the bounty payout multiplier for a specific contributor, and provide the reason for why. \n example usage: "/wallet @user 0.5 'Multiplier reason'"`, + handler: multiplier, + callback: commandCallback, + }, + { + id: IssueCommentCommands.ALLOW, + description: `Set access control. (Admin Only)`, + handler: setAccess, + callback: commandCallback, + }, + { + id: IssueCommentCommands.WALLET, + description: config.wallet.registerWalletWithVerification + ? ` : Register the hunter's wallet address. \n Your message to sign is: DevPool\n You can generate SIGNATURE_HASH at https://etherscan.io/verifiedSignatures\n ex1: /wallet 0x0000000000000000000000000000000000000000 0xe2a3e34a63f3def2c29605de82225b79e1398190b542be917ef88a8e93ff9dc91bdc3ef9b12ed711550f6d2cbbb50671aa3f14a665b709ec391f3e603d0899a41b\n ex2: /wallet vitalik.eth 0x75329f883590507e581cd6dfca62680b6cd12e1f1665db8097f9e642ed70025146b5cf9f777dde90c4a9cbd41500a6bf76bc394fd0b0cae2aab09f7a6f30e3b31b\n` + : `: Register the hunter's wallet address. \n ex1: /wallet 0x0000000000000000000000000000000000000000\n ex2: /wallet vitalik.eth\n`, + handler: registerWallet, + callback: commandCallback, + }, + ]; +}; diff --git a/src/handlers/comment/handlers/multiplier.ts b/src/handlers/comment/handlers/multiplier.ts index c54ac2ad3..bef7a68b3 100644 --- a/src/handlers/comment/handlers/multiplier.ts +++ b/src/handlers/comment/handlers/multiplier.ts @@ -49,7 +49,7 @@ export const multiplier = async (body: string) => { reason += part.replace(/['"]/g, "") + " "; } } - + username = username || sender; // check if sender is admin or billing_manager // passing in context so we don't have to make another request to get the user const permissionLevel = await getUserPermission(sender, context); @@ -66,12 +66,14 @@ export const multiplier = async (body: string) => { } } + logger.info(`Upserting to the wallet table, username: ${username}, bountyMultiplier: ${bountyMultiplier}, reason: ${reason}}`); + await upsertWalletMultiplier(username, bountyMultiplier?.toString(), reason); return `Successfully changed the payout multiplier for @${username} to ${bountyMultiplier}. The reason ${ reason ? `provided is "${reason}"` : "is not provided" }.`; } else { logger.error("Invalid body for bountyMultiplier command"); - return `Invalid body for bountyMultiplier command`; + return `Invalid syntax for wallet command \n example usage: "/multiplier @user 0.5 'Multiplier reason'"`; } }; diff --git a/src/handlers/comment/handlers/query.ts b/src/handlers/comment/handlers/query.ts new file mode 100644 index 000000000..a31b71b6c --- /dev/null +++ b/src/handlers/comment/handlers/query.ts @@ -0,0 +1,34 @@ +import { getWalletInfo } from "../../../adapters/supabase"; +import { getBotContext, getLogger } from "../../../bindings"; +import { Payload } from "../../../types"; + +export const query = async (body: string) => { + const context = getBotContext(); + const logger = getLogger(); + const payload = context.payload as Payload; + const sender = payload.sender.login; + + logger.info(`Received '/query' command from user: ${sender}`); + + const issue = payload.issue; + if (!issue) { + logger.info(`Skipping '/query' because of no issue instance`); + return; + } + + const regex = /\/query @([A-Za-z0-9_]+)/gm; + const matches = body.match(regex); + const user = matches?.[1]; + + if (user) { + const walletInfo = await getWalletInfo(user); + if (typeof walletInfo == "number") { + return `Error retrieving multiplier and wallet address for @${user}`; + } else { + return `@${user}'s wallet address is ${walletInfo?.address} and multiplier is ${walletInfo?.multiplier}`; + } + } else { + logger.error("Invalid body for query command"); + return `Invalid syntax for query command \n usage /query @user`; + } +}; diff --git a/src/handlers/comment/handlers/table.ts b/src/handlers/comment/handlers/table.ts index 547e0d866..fef8e181e 100644 --- a/src/handlers/comment/handlers/table.ts +++ b/src/handlers/comment/handlers/table.ts @@ -1,28 +1,44 @@ -export const tableComment = (deadline: string, wallet: string, multiplier: string, reason: string, bouty: string) => { - return ` +export const tableComment = ({ + deadline, + wallet, + multiplier, + reason, + bounty, +}: { + deadline: string; + wallet: string; + multiplier: string; + reason: string; + bounty: string; +}) => { + return ` + + +
      - - + + - - + + - - + + - - + + - - + + - - + + -
      Deadline${deadline}Deadline${deadline}
      Registered Wallet${wallet}Registered Wallet${wallet}
      Payment Multiplier${multiplier}Payment Multiplier${multiplier}
      Multiplier Reason${reason}Multiplier Reason${reason}
      Total Bounty${bouty}Total Bounty${bounty}
      `; + +`; }; diff --git a/src/handlers/comment/handlers/unassign.ts b/src/handlers/comment/handlers/unassign.ts index fb44edae9..3289ed0b0 100644 --- a/src/handlers/comment/handlers/unassign.ts +++ b/src/handlers/comment/handlers/unassign.ts @@ -2,6 +2,7 @@ import { removeAssignees } from "../../../helpers"; import { getBotContext, getLogger } from "../../../bindings"; import { Payload } from "../../../types"; import { IssueCommentCommands } from "../commands"; +import { closePullRequestForAnIssue } from "../../assign/index"; export const unassign = async (body: string) => { const { payload: _payload } = getBotContext(); @@ -27,6 +28,7 @@ export const unassign = async (body: string) => { logger.debug(`Unassigning sender: ${payload.sender.login.toLowerCase()}, assignee: ${assignees[0].login.toLowerCase()}, shouldUnassign: ${shouldUnassign}`); if (shouldUnassign) { + await closePullRequestForAnIssue(); await removeAssignees( issue_number, assignees.map((i) => i.login) diff --git a/src/handlers/comment/handlers/wallet.ts b/src/handlers/comment/handlers/wallet.ts index a100360a1..6cabfd357 100644 --- a/src/handlers/comment/handlers/wallet.ts +++ b/src/handlers/comment/handlers/wallet.ts @@ -1,10 +1,11 @@ +import { ethers } from "ethers"; import { upsertWalletAddress } from "../../../adapters/supabase"; -import { getBotContext, getLogger } from "../../../bindings"; +import { getBotConfig, getBotContext, getLogger } from "../../../bindings"; import { resolveAddress } from "../../../helpers"; import { Payload } from "../../../types"; import { formatEthAddress } from "../../../utils"; import { IssueCommentCommands } from "../commands"; - +import { constants } from "ethers"; // Extracts ensname from raw text. const extractEnsName = (text: string): string | undefined => { // Define a regular expression to match ENS names @@ -23,6 +24,7 @@ const extractEnsName = (text: string): string | undefined => { export const registerWallet = async (body: string) => { const { payload: _payload } = getBotContext(); + const config = getBotConfig(); const logger = getLogger(); const payload = _payload as Payload; const sender = payload.sender.login; @@ -34,7 +36,10 @@ export const registerWallet = async (body: string) => { if (!address && !ensName) { logger.info("Skipping to register a wallet address because both address/ens doesn't exist"); - return; + if (config.wallet.registerWalletWithVerification) { + return `Please include your wallet or ENS address.\n usage: /wallet 0x0000000000000000000000000000000000000000 0x0830f316c982a7fd4ff050c8fdc1212a8fd92f6bb42b2337b839f2b4e156f05a359ef8f4acd0b57cdedec7874a865ee07076ab2c81dc9f9de28ced55228587f81c`; + } + return `Please include your wallet or ENS address.\n usage: /wallet 0x0000000000000000000000000000000000000000`; } if (!address && ensName) { @@ -47,7 +52,33 @@ export const registerWallet = async (body: string) => { logger.info(`Resolved address from Ens name: ${ensName}, address: ${address}`); } + if (config.wallet.registerWalletWithVerification) { + const regexForSigHash = /(0x[a-fA-F0-9]{130})/g; + const sigHashMatches = body.match(regexForSigHash); + const sigHash = sigHashMatches ? sigHashMatches[0] : null; + + const messageToSign = "DevPool"; + const failedSigLogMsg = `Skipping to register the wallet address because you have not provided a valid SIGNATURE_HASH.`; + const failedSigResponse = `Skipping to register the wallet address because you have not provided a valid SIGNATURE_HASH. \nUse [etherscan](https://etherscan.io/verifiedSignatures) to sign the message \`${messageToSign}\` and register your wallet by appending the signature hash.\n\n**Usage:**\n/wallet \n\n**Example:**\n/wallet 0x0000000000000000000000000000000000000000 0x0830f316c982a7fd4ff050c8fdc1212a8fd92f6bb42b2337b839f2b4e156f05a359ef8f4acd0b57cdedec7874a865ee07076ab2c81dc9f9de28ced55228587f81c`; + try { + //verifyMessage throws an error when some parts(r,s,v) of the signature are correct but some are not + const isSigHashValid = address && sigHash && ethers.utils.verifyMessage(messageToSign, sigHash) == ethers.utils.getAddress(address); + if (!isSigHashValid) { + logger.info(failedSigLogMsg); + return failedSigResponse; + } + } catch (e) { + logger.info(`Exception thrown by verifyMessage for /wallet: ${e}`); + logger.info(failedSigLogMsg); + return failedSigResponse; + } + } + if (address) { + if (address == constants.AddressZero) { + logger.info("Skipping to register a wallet address because user is trying to set their address to null address"); + return `Cannot set address to null address`; + } await upsertWalletAddress(sender, address); return `Updated the wallet address for @${sender} successfully!\t Your new address: ${formatEthAddress(address)}`; } diff --git a/src/handlers/payout/action.ts b/src/handlers/payout/action.ts index 4d33917a5..e998bba5c 100644 --- a/src/handlers/payout/action.ts +++ b/src/handlers/payout/action.ts @@ -1,7 +1,7 @@ import { getWalletAddress, getWalletMultiplier } from "../../adapters/supabase"; import { getBotConfig, getBotContext, getLogger } from "../../bindings"; import { addLabelToIssue, deleteLabel, generatePermit2Signature, getAllIssueComments, getTokenSymbol } from "../../helpers"; -import { Payload, StateReason } from "../../types"; +import { UserType, Payload, StateReason } from "../../types"; import { shortenEthAddress } from "../../utils"; import { bountyInfo } from "../wildcard"; @@ -48,7 +48,7 @@ export const handleIssueClosed = async () => { const multiplier = await getWalletMultiplier(assignee.login); if (multiplier === 0) { - const errMsg = "Refusing to generate the payment permit because" + `@${assignee.login}` + "'s payment `multiplier` is `0`"; + const errMsg = "Refusing to generate the payment permit because " + `@${assignee.login}` + "'s payment `multiplier` is `0`"; logger.info(errMsg); return errMsg; } @@ -57,15 +57,7 @@ export const handleIssueClosed = async () => { const priceInEth = (+issueDetailed.priceLabel.substring(7, issueDetailed.priceLabel.length - 4) * multiplier).toString(); if (!recipient || recipient?.trim() === "") { logger.info(`Recipient address is missing`); - return ( - "Please set your wallet address by using the `/wallet` command.\n" + - "```\n" + - "/wallet example.eth\n" + - "/wallet 0xBf...CdA\n" + - "```\n" + - "@" + - assignee.login - ); + return; } const payoutUrl = await generatePermit2Signature(recipient, priceInEth, issue.node_id); @@ -74,9 +66,8 @@ export const handleIssueClosed = async () => { logger.info(`Posting a payout url to the issue, url: ${payoutUrl}`); const comment = `### [ **[ CLAIM ${priceInEth} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n` + "```" + shortenRecipient + "```"; const comments = await getAllIssueComments(issue.number); - const commentContents = comments.map((i) => i.body); - const exist = commentContents.find((content) => content.includes(comment)); - if (exist) { + const permitComments = comments.filter((content) => content.body.includes("https://pay.ubq.fi?claim=") && content.user.type == UserType.Bot); + if (permitComments.length > 0) { logger.info(`Skip to generate a permit url because it has been already posted`); return `Permit generation skipped because it was already posted to this issue.`; } diff --git a/src/handlers/processors.ts b/src/handlers/processors.ts index 340c091f3..ecd47fcd5 100644 --- a/src/handlers/processors.ts +++ b/src/handlers/processors.ts @@ -3,7 +3,7 @@ import { closePullRequestForAnIssue, commentWithAssignMessage } from "./assign"; import { pricingLabelLogic, validatePriceLabels } from "./pricing"; import { checkBountiesToUnassign, collectAnalytics, checkWeeklyUpdate } from "./wildcard"; import { nullHandler } from "./shared"; -import { handleComment, issueClosedCallback, issueCreatedCallback } from "./comment"; +import { handleComment, issueClosedCallback } from "./comment"; import { checkPullRequests } from "./assign/auto"; import { createDevPoolPR } from "./pull-request"; import { runOnPush } from "./push"; @@ -12,7 +12,7 @@ import { incentivizeComments, incentivizeCreatorComment } from "./payout"; export const processors: Record = { [GithubEvent.ISSUES_OPENED]: { pre: [nullHandler], - action: [issueCreatedCallback], + action: [nullHandler], // SHOULD not set `issueCreatedCallback` until the exploit issue resolved. https://github.com/ubiquity/ubiquibot/issues/535 post: [nullHandler], }, [GithubEvent.ISSUES_LABELED]: { diff --git a/src/handlers/pull-request/create-devpool-pr.ts b/src/handlers/pull-request/create-devpool-pr.ts index 75689171f..08046bfbc 100644 --- a/src/handlers/pull-request/create-devpool-pr.ts +++ b/src/handlers/pull-request/create-devpool-pr.ts @@ -8,7 +8,7 @@ export const createDevPoolPR = async () => { const payload = context.payload as Payload; const devPoolOwner = "ubiquity"; - const devPoolRepo = "devpool"; + const devPoolRepo = "devpool-directory"; if (!payload.repositories_added) { return; diff --git a/src/handlers/push/update-base.ts b/src/handlers/push/update-base.ts index 6f10418de..4141d811f 100644 --- a/src/handlers/push/update-base.ts +++ b/src/handlers/push/update-base.ts @@ -21,12 +21,12 @@ export const updateBaseRate = async (context: Context, payload: PushPayload, fil const previousContent = Buffer.from(preFileContent, "base64").toString(); const previousConfig = await parseYAML(previousContent); - if (!previousConfig || !previousConfig["base-multiplier"]) { + if (!previousConfig || !previousConfig["price-multiplier"]) { logger.debug("No multiplier found in file object"); return; } - const previousBaseRate = previousConfig["base-multiplier"]; + const previousBaseRate = previousConfig["price-multiplier"]; // fetch all labels const repoLabels = await listLabelsForRepo(); diff --git a/src/handlers/shared/pricing.ts b/src/handlers/shared/pricing.ts index f422ca84c..65c1079d0 100644 --- a/src/handlers/shared/pricing.ts +++ b/src/handlers/shared/pricing.ts @@ -4,7 +4,7 @@ export const calculateBountyPrice = (timeValue: number, priorityValue: number, b const botConfig = getBotConfig(); const base = baseValue ?? botConfig.price.baseMultiplier; const priority = priorityValue / 10; // floats cause bad math - const price = base * timeValue * priority; + const price = 1000 * base * timeValue * priority; return price; }; diff --git a/src/handlers/wildcard/analytics.ts b/src/handlers/wildcard/analytics.ts index e88e91898..40e861ac7 100644 --- a/src/handlers/wildcard/analytics.ts +++ b/src/handlers/wildcard/analytics.ts @@ -43,10 +43,10 @@ export const bountyInfo = ( export const collectAnalytics = async (): Promise => { const logger = getLogger(); const { - mode: { analyticsMode }, + mode: { disableAnalytics }, } = getBotConfig(); - if (!analyticsMode) { - logger.info(`Skipping to collect analytics, reason: mode=${analyticsMode}`); + if (disableAnalytics) { + logger.info(`Skipping to collect analytics, reason: mode=${disableAnalytics}`); return; } logger.info("Collecting analytics information..."); diff --git a/src/handlers/wildcard/unassign.ts b/src/handlers/wildcard/unassign.ts index c5f4854fc..b94b2c1c7 100644 --- a/src/handlers/wildcard/unassign.ts +++ b/src/handlers/wildcard/unassign.ts @@ -1,11 +1,12 @@ +import { closePullRequestForAnIssue } from "../assign"; import { getBotConfig, getLogger } from "../../bindings"; import { GLOBAL_STRINGS } from "../../configs/strings"; import { addCommentToIssue, - getCommentsOfIssue, + getAllIssueComments, getCommitsOnPullRequest, getOpenedPullRequestsForAnIssue, - listIssuesForRepo, + listAllIssuesForRepo, removeAssignees, } from "../../helpers"; import { Comment, Issue, IssueType } from "../../types"; @@ -20,7 +21,7 @@ export const checkBountiesToUnassign = async () => { // List all the issues in the repository. It may include `pull_request` // because GitHub's REST API v3 considers every pull request an issue - const issues_opened = await listIssuesForRepo(IssueType.OPEN); + const issues_opened = await listAllIssuesForRepo(IssueType.OPEN); const assigned_issues = issues_opened.filter((issue) => issue.assignee); @@ -37,15 +38,16 @@ const checkBountyToUnassign = async (issue: Issue): Promise => { logger.info(`Checking the bounty to unassign, issue_number: ${issue.number}`); const { unassignComment, askUpdate } = GLOBAL_STRINGS; const assignees = issue.assignees.map((i) => i.login); - const comments = await getCommentsOfIssue(issue.number); + const comments = await getAllIssueComments(issue.number); if (!comments || comments.length == 0) return false; const askUpdateComments = comments .filter((comment: Comment) => comment.body.includes(askUpdate)) .sort((a: Comment, b: Comment) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); + const lastAskTime = askUpdateComments.length > 0 ? new Date(askUpdateComments[0].created_at).getTime() : new Date(issue.created_at).getTime(); const curTimestamp = new Date().getTime(); - const lastActivity = await lastActivityTime(issue); + const lastActivity = await lastActivityTime(issue, comments); const passedDuration = curTimestamp - lastActivity.getTime(); if (passedDuration >= disqualifyTime || passedDuration >= followUpTime) { @@ -53,9 +55,10 @@ const checkBountyToUnassign = async (issue: Issue): Promise => { logger.info( `Unassigning... lastActivityTime: ${lastActivity.getTime()}, curTime: ${curTimestamp}, passedDuration: ${passedDuration}, followUpTime: ${followUpTime}, disqualifyTime: ${disqualifyTime}` ); + await closePullRequestForAnIssue(); // remove assignees from the issue await removeAssignees(issue.number, assignees); - await addCommentToIssue(`${unassignComment} \nLast activity time: ${lastActivity}`, issue.number); + await addCommentToIssue(`@${assignees[0]} - ${unassignComment} \nLast activity time: ${lastActivity}`, issue.number); return true; } else if (passedDuration >= followUpTime) { @@ -78,14 +81,14 @@ const checkBountyToUnassign = async (issue: Issue): Promise => { return false; }; -const lastActivityTime = async (issue: Issue): Promise => { +const lastActivityTime = async (issue: Issue, comments: Comment[]): Promise => { const logger = getLogger(); logger.info(`Checking the latest activity for the issue, issue_number: ${issue.number}`); const assignees = issue.assignees.map((i) => i.login); const activities: Date[] = [new Date(issue.created_at)]; // get last comment on the issue - const lastCommentsOfHunterForIssue = (await getCommentsOfIssue(issue.number)) + const lastCommentsOfHunterForIssue = comments .filter((comment) => assignees.includes(comment.user.login)) .sort((a: Comment, b: Comment) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); @@ -98,9 +101,9 @@ const lastActivityTime = async (issue: Issue): Promise => { const commits = (await getCommitsOnPullRequest(pr.number)) .filter((it) => it.commit.committer?.date) .sort((a, b) => new Date(b.commit.committer?.date ?? 0).getTime() - new Date(a.commit.committer?.date ?? 0).getTime()); - const prComments = (await getCommentsOfIssue(pr.number)) + const prComments = (await getAllIssueComments(pr.number)) .filter((comment) => comment.user.login === assignees[0]) - .sort((a, b) => new Date(a.created_at).getTime() - new Date(b.created_at).getTime()); + .sort((a, b) => new Date(b.created_at).getTime() - new Date(a.created_at).getTime()); if (commits.length > 0) activities.push(new Date(commits[0].commit.committer?.date ?? 0)); if (prComments.length > 0) activities.push(new Date(prComments[0].created_at)); diff --git a/src/handlers/wildcard/weekly.ts b/src/handlers/wildcard/weekly.ts index 7f05cf077..3f52bcd5b 100644 --- a/src/handlers/wildcard/weekly.ts +++ b/src/handlers/wildcard/weekly.ts @@ -7,10 +7,10 @@ const SEVEN_DAYS = 604800; // 7 days in seconds export const checkWeeklyUpdate = async () => { const { log } = getBotContext(); const { - mode: { analyticsMode }, + mode: { disableAnalytics }, } = getBotConfig(); - if (!analyticsMode) { - log.info(`Skipping to collect the weekly analytics, reason: mode=${analyticsMode}`); + if (disableAnalytics) { + log.info(`Skipping to collect the weekly analytics, reason: mode=${disableAnalytics}`); return; } const curTime = Date.now() / 1000; diff --git a/src/helpers/issue.ts b/src/helpers/issue.ts index 01cf8bf85..ae9508f00 100644 --- a/src/helpers/issue.ts +++ b/src/helpers/issue.ts @@ -60,6 +60,8 @@ export const listIssuesForRepo = async (state: "open" | "closed" | "all" = "open page, }); + await checkRateLimitGit(response.headers); + if (response.status === 200) { return response.data; } else { @@ -67,6 +69,24 @@ export const listIssuesForRepo = async (state: "open" | "closed" | "all" = "open } }; +export const listAllIssuesForRepo = async (state: "open" | "closed" | "all" = "open") => { + const issuesArr = []; + let fetchDone = false; + const perPage = 100; + let curPage = 1; + while (!fetchDone) { + const issues = await listIssuesForRepo(state, perPage, curPage); + + // push the objects to array + issuesArr.push(...issues); + + if (issues.length < perPage) fetchDone = true; + else curPage++; + } + + return issuesArr; +}; + export const addCommentToIssue = async (msg: string, issue_number: number) => { const context = getBotContext(); const logger = getLogger(); @@ -84,6 +104,56 @@ export const addCommentToIssue = async (msg: string, issue_number: number) => { } }; +export const updateCommentOfIssue = async (msg: string, issue_number: number, reply_to: Comment) => { + const context = getBotContext(); + const logger = getLogger(); + const payload = context.payload as Payload; + + try { + const appResponse = await context.octokit.apps.getAuthenticated(); + const { name, slug } = appResponse.data; + logger.info(`App name/slug ${name}/${slug}`); + + const editCommentBy = `${slug}[bot]`; + logger.info(`Bot slug: ${editCommentBy}`); + + const comments = await context.octokit.issues.listComments({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + issue_number: issue_number, + since: reply_to.created_at, + per_page: 30, + }); + + const comment_to_edit = comments.data.find((comment) => { + return comment?.user?.login == editCommentBy && comment.id > reply_to.id; + }); + + if (comment_to_edit) { + logger.info(`For comment_id: ${reply_to.id} found comment_to_edit with id: ${comment_to_edit.id}`); + await context.octokit.issues.updateComment({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + comment_id: comment_to_edit.id, + body: msg, + }); + } else { + logger.info(`Falling back to add comment. Couldn't find response to edit for comment_id: ${reply_to.id}`); + await addCommentToIssue(msg, issue_number); + } + } catch (e: unknown) { + logger.debug(`Upading a comment failed!, reason: ${e}`); + } +}; + +export const upsertCommentToIssue = async (issue_number: number, comment: string, action: string, reply_to?: Comment) => { + if (action == "edited" && reply_to) { + await updateCommentOfIssue(comment, issue_number, reply_to); + } else { + await addCommentToIssue(comment, issue_number); + } +}; + export const getCommentsOfIssue = async (issue_number: number): Promise => { const context = getBotContext(); const logger = getLogger(); @@ -344,6 +414,18 @@ export const getIssueByNumber = async (context: Context, issue_number: number) = } }; +export const getPullByNumber = async (context: Context, pull_number: number) => { + const logger = getLogger(); + const payload = context.payload as Payload; + try { + const { data: pull } = await context.octokit.rest.pulls.get({ owner: payload.repository.owner.login, repo: payload.repository.name, pull_number }); + return pull; + } catch (error) { + logger.debug(`Fetching pull failed!, reason: ${error}`); + return; + } +}; + // Get issues assigned to a username export const getAssignedIssues = async (username: string) => { const issuesArr = []; @@ -415,7 +497,10 @@ export const getAvailableOpenedPullRequests = async (username: string) => { const pr = opened_prs[i]; const reviews = await getAllPullRequestReviews(context, pr.number); - if (reviews.length > 0) result.push(pr); + if (reviews.length > 0) { + const approvedReviews = reviews.find((review) => review.state === "APPROVED"); + if (approvedReviews) result.push(pr); + } if (reviews.length === 0 && (new Date().getTime() - new Date(pr.created_at).getTime()) / (1000 * 60 * 60) >= DEFAULT_TIME_RANGE_FOR_MAX_ISSUE) { result.push(pr); diff --git a/src/helpers/payout.ts b/src/helpers/payout.ts index 361920060..7ef127f77 100644 --- a/src/helpers/payout.ts +++ b/src/helpers/payout.ts @@ -16,7 +16,7 @@ import { DEFAULT_RPC_ENDPOINT } from "../configs"; import { PayoutConfigSchema } from "../types"; // available tokens for payouts -const PAYMENT_TOKEN_PER_CHAIN: Record = { +const PAYMENT_TOKEN_PER_NETWORK: Record = { "1": { rpc: DEFAULT_RPC_ENDPOINT, token: "0x6B175474E89094C44Da98b954EedeAC495271d0F", // DAI @@ -27,17 +27,17 @@ const PAYMENT_TOKEN_PER_CHAIN: Record = }, }; -type PayoutConfigPartial = Omit, "chainId" | "privateKey" | "permitBaseUrl">; +type PayoutConfigPartial = Omit, "networkId" | "privateKey" | "permitBaseUrl">; /** - * Returns payout config for a particular chain - * @param chainId chain id + * Returns payout config for a particular network + * @param networkId network id * @returns RPC URL and payment token */ -export const getPayoutConfigByChainId = (chainId: number): PayoutConfigPartial => { - const paymentToken = PAYMENT_TOKEN_PER_CHAIN[chainId.toString()]; +export const getPayoutConfigByNetworkId = (networkId: number): PayoutConfigPartial => { + const paymentToken = PAYMENT_TOKEN_PER_NETWORK[networkId.toString()]; if (!paymentToken) { - throw new Error(`No config setup for chainId: ${chainId}`); + throw new Error(`No config setup for networkId: ${networkId}`); } return { diff --git a/src/helpers/permit.ts b/src/helpers/permit.ts index 290064230..de17b98eb 100644 --- a/src/helpers/permit.ts +++ b/src/helpers/permit.ts @@ -3,7 +3,7 @@ import { BigNumber, ethers } from "ethers"; import { getBotConfig, getLogger } from "../bindings"; import { keccak256, toUtf8Bytes } from "ethers/lib/utils"; -const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // same on all chains +const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // same on all networks /** * Generates permit2 signature data with `spender` and `amountInETH` @@ -15,7 +15,7 @@ const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // same on */ export const generatePermit2Signature = async (spender: string, amountInEth: string, identifier: string): Promise => { const { - payout: { chainId, privateKey, permitBaseUrl, rpc, paymentToken }, + payout: { networkId, privateKey, permitBaseUrl, rpc, paymentToken }, } = getBotConfig(); const logger = getLogger(); const provider = new ethers.providers.JsonRpcProvider(rpc); @@ -35,7 +35,7 @@ export const generatePermit2Signature = async (spender: string, amountInEth: str deadline: MaxUint256, }; - const { domain, types, values } = SignatureTransfer.getPermitData(permitTransferFromData, PERMIT2_ADDRESS, chainId); + const { domain, types, values } = SignatureTransfer.getPermitData(permitTransferFromData, PERMIT2_ADDRESS, networkId); const signature = await adminWallet._signTypedData(domain, types, values); const txData = { @@ -57,7 +57,7 @@ export const generatePermit2Signature = async (spender: string, amountInEth: str const base64encodedTxData = Buffer.from(JSON.stringify(txData)).toString("base64"); - const result = `${permitBaseUrl}?claim=${base64encodedTxData}&network=${chainId}`; + const result = `${permitBaseUrl}?claim=${base64encodedTxData}&network=${networkId}`; logger.info(`Generated permit2 url: ${result}`); return result; }; diff --git a/src/types/config.ts b/src/types/config.ts index 56d48079a..855ba2049 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -16,6 +16,7 @@ export const PriceConfigSchema = Type.Object({ timeLabels: Type.Array(LabelItemSchema), priorityLabels: Type.Array(LabelItemSchema), commentElementPricing: CommentElementPricingSchema, + defaultLabels: Type.Array(Type.String()), }); export type PriceConfig = Static; @@ -30,7 +31,7 @@ export const TelegramBotConfigSchema = Type.Object({ }); export const PayoutConfigSchema = Type.Object({ - chainId: Type.Number(), + networkId: Type.Number(), rpc: Type.String(), privateKey: Type.String(), paymentToken: Type.String(), @@ -44,7 +45,7 @@ export const UnassignConfigSchema = Type.Object({ export const ModeSchema = Type.Object({ autoPayMode: Type.Boolean(), - analyticsMode: Type.Boolean(), + disableAnalytics: Type.Boolean(), incentiveMode: Type.Boolean(), }); @@ -62,6 +63,14 @@ export const SodiumSchema = Type.Object({ privateKey: Type.String(), }); +export const CommentsSchema = Type.Object({ + promotionComment: Type.String(), +}); + +export const WalletSchema = Type.Object({ + registerWalletWithVerification: Type.Boolean(), +}); + export const BotConfigSchema = Type.Object({ log: LogConfigSchema, price: PriceConfigSchema, @@ -72,6 +81,8 @@ export const BotConfigSchema = Type.Object({ mode: ModeSchema, assign: AssignSchema, sodium: SodiumSchema, + comments: CommentsSchema, + wallet: WalletSchema, }); export type BotConfig = Static; diff --git a/src/types/handlers.ts b/src/types/handlers.ts index 804f8769d..5a79f4dd0 100644 --- a/src/types/handlers.ts +++ b/src/types/handlers.ts @@ -1,6 +1,8 @@ +import { Comment } from "./payload"; + export type CommandsHandler = (args: string) => Promise; export type ActionHandler = (args?: string) => Promise; -export type CallbackHandler = (issue_number: number, text: string) => Promise; +export type CallbackHandler = (issue_number: number, text: string, action: string, reply_to?: Comment) => Promise; export type PreActionHandler = ActionHandler; export type PostActionHandler = ActionHandler; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index e8e41fc6c..415305870 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -7,23 +7,23 @@ interface Configs { parsedDefault: WideRepoConfig; } -export const getChainId = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): number => { - if (parsedRepo && parsedRepo["chain-id"] && !Number.isNaN(Number(parsedRepo["chain-id"]))) { - return Number(parsedRepo["chain-id"]); - } else if (parsedOrg && parsedOrg["chain-id"] && !Number.isNaN(Number(parsedOrg["chain-id"]))) { - return Number(parsedOrg["chain-id"]); +export const getNetworkId = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): number => { + if (parsedRepo && parsedRepo["evm-network-id"] && !Number.isNaN(Number(parsedRepo["evm-network-id"]))) { + return Number(parsedRepo["evm-network-id"]); + } else if (parsedOrg && parsedOrg["evm-network-id"] && !Number.isNaN(Number(parsedOrg["evm-network-id"]))) { + return Number(parsedOrg["evm-network-id"]); } else { - return Number(parsedDefault["chain-id"]!); + return Number(parsedDefault["evm-network-id"]); } }; export const getBaseMultiplier = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): number => { - if (parsedRepo && parsedRepo["base-multiplier"] && !Number.isNaN(Number(parsedRepo["base-multiplier"]))) { - return Number(parsedRepo["base-multiplier"]); - } else if (parsedOrg && parsedOrg["base-multiplier"] && !Number.isNaN(Number(parsedOrg["base-multiplier"]))) { - return Number(parsedOrg["base-multiplier"]); + if (parsedRepo && parsedRepo["price-multiplier"] && !Number.isNaN(Number(parsedRepo["price-multiplier"]))) { + return Number(parsedRepo["price-multiplier"]); + } else if (parsedOrg && parsedOrg["price-multiplier"] && !Number.isNaN(Number(parsedOrg["price-multiplier"]))) { + return Number(parsedOrg["price-multiplier"]); } else { - return Number(parsedDefault["base-multiplier"]!); + return Number(parsedDefault["price-multiplier"]); } }; @@ -43,7 +43,7 @@ export const getTimeLabels = ({ parsedRepo, parsedOrg, parsedDefault }: Configs) } else if (parsedOrg && parsedOrg["time-labels"] && Array.isArray(parsedOrg["time-labels"]) && parsedOrg["time-labels"].length > 0) { return parsedOrg["time-labels"]; } else { - return parsedDefault["time-labels"]!; + return parsedDefault["time-labels"] as WideLabel[]; } }; @@ -53,7 +53,7 @@ export const getPriorityLabels = ({ parsedRepo, parsedOrg, parsedDefault }: Conf } else if (parsedOrg && parsedOrg["priority-labels"] && Array.isArray(parsedOrg["priority-labels"]) && parsedOrg["priority-labels"].length > 0) { return parsedOrg["priority-labels"]; } else { - return parsedDefault["priority-labels"]!; + return parsedDefault["priority-labels"] as WideLabel[]; } }; @@ -63,7 +63,7 @@ export const getCommentItemPrice = ({ parsedRepo, parsedOrg, parsedDefault }: Co } else if (parsedOrg && parsedOrg["comment-element-pricing"]) { return parsedOrg["comment-element-pricing"]; } else { - return parsedDefault["comment-element-pricing"]!; + return parsedDefault["comment-element-pricing"] as CommentElementPricing; } }; @@ -73,36 +73,66 @@ export const getAutoPayMode = ({ parsedRepo, parsedOrg, parsedDefault }: Configs } else if (parsedOrg && parsedOrg["auto-pay-mode"] && typeof parsedOrg["auto-pay-mode"] === "boolean") { return parsedOrg["auto-pay-mode"]; } else { - return parsedDefault["auto-pay-mode"]!; + return parsedDefault["auto-pay-mode"] as boolean; } }; export const getAnalyticsMode = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): boolean => { - if (parsedRepo && parsedRepo["analytics-mode"] && typeof parsedRepo["analytics-mode"] === "boolean") { - return parsedRepo["analytics-mode"]; - } else if (parsedOrg && parsedOrg["analytics-mode"] && typeof parsedOrg["analytics-mode"] === "boolean") { - return parsedOrg["analytics-mode"]; + if (parsedRepo && parsedRepo["disable-analytics"] && typeof parsedRepo["disable-analytics"] === "boolean") { + return parsedRepo["disable-analytics"]; + } else if (parsedOrg && parsedOrg["disable-analytics"] && typeof parsedOrg["disable-analytics"] === "boolean") { + return parsedOrg["disable-analytics"]; } else { - return parsedDefault["analytics-mode"]!; + return parsedDefault["disable-analytics"] as boolean; + } +}; + +export const getPromotionComment = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): string => { + if (parsedRepo && parsedRepo["promotion-comment"] && typeof parsedRepo["promotion-comment"] === "string") { + return parsedRepo["promotion-comment"]; + } else if (parsedOrg && parsedOrg["promotion-comment"] && typeof parsedOrg["promotion-comment"] === "string") { + return parsedOrg["promotion-comment"]; + } else { + return parsedDefault["promotion-comment"] as string; } }; export const getIncentiveMode = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): boolean => { - if (parsedRepo && parsedRepo["incentive-mode"] && typeof parsedRepo["incentive-mode"] === "boolean") { - return parsedRepo["incentive-mode"]; - } else if (parsedOrg && parsedOrg["incentive-mode"] && typeof parsedOrg["incentive-mode"] === "boolean") { - return parsedOrg["incentive-mode"]; + if (parsedRepo && parsedRepo["comment-incentives"] && typeof parsedRepo["comment-incentives"] === "boolean") { + return parsedRepo["comment-incentives"]; + } else if (parsedOrg && parsedOrg["comment-incentives"] && typeof parsedOrg["comment-incentives"] === "boolean") { + return parsedOrg["comment-incentives"]; } else { - return parsedDefault["incentive-mode"]!; + return parsedDefault["comment-incentives"] as boolean; } }; export const getBountyHunterMax = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): number => { - if (parsedRepo && parsedRepo["max-concurrent-bounties"] && !Number.isNaN(Number(parsedRepo["max-concurrent-bounties"]))) { - return Number(parsedRepo["max-concurrent-bounties"]); - } else if (parsedOrg && parsedOrg["max-concurrent-bounties"] && !Number.isNaN(Number(parsedOrg["max-concurrent-bounties"]))) { - return Number(parsedOrg["max-concurrent-bounties"]); + if (parsedRepo && parsedRepo["max-concurrent-assigns"] && !Number.isNaN(Number(parsedRepo["max-concurrent-assigns"]))) { + return Number(parsedRepo["max-concurrent-assigns"]); + } else if (parsedOrg && parsedOrg["max-concurrent-assigns"] && !Number.isNaN(Number(parsedOrg["max-concurrent-assigns"]))) { + return Number(parsedOrg["max-concurrent-assigns"]); + } else { + return Number(parsedDefault["max-concurrent-assigns"]); + } +}; + +export const getDefaultLabels = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): string[] => { + if (parsedRepo && parsedRepo["default-labels"]) { + return parsedRepo["default-labels"]; + } else if (parsedOrg && parsedOrg["default-labels"]) { + return parsedOrg["default-labels"]; + } else { + return parsedDefault["default-labels"] as string[]; + } +}; + +export const getRegisterWalletWithVerification = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): boolean => { + if (parsedRepo && parsedRepo["register-wallet-with-verification"] && typeof parsedRepo["register-wallet-with-verification"] === "boolean") { + return Boolean(parsedRepo["register-wallet-with-verification"]); + } else if (parsedOrg && parsedOrg["register-wallet-with-verification"] && typeof parsedOrg["register-wallet-with-verification"] === "boolean") { + return Boolean(parsedOrg["register-wallet-with-verification"]); } else { - return Number(parsedDefault["max-concurrent-bounties"]!); + return Boolean(parsedDefault["register-wallet-with-verification"]); } }; diff --git a/src/utils/private.ts b/src/utils/private.ts index 66e07bd47..0a6e98064 100644 --- a/src/utils/private.ts +++ b/src/utils/private.ts @@ -10,10 +10,13 @@ import { getCreatorMultiplier, getBountyHunterMax, getIncentiveMode, - getChainId, + getNetworkId, getPriorityLabels, getTimeLabels, getCommentItemPrice, + getDefaultLabels, + getPromotionComment, + getRegisterWalletWithVerification, } from "./helpers"; const CONFIG_REPO = "ubiquibot-config"; @@ -48,16 +51,19 @@ export interface WideLabel { } export interface WideConfig { - "chain-id"?: number; - "base-multiplier"?: number; + "evm-network-id"?: number; + "price-multiplier"?: number; "issue-creator-multiplier": number; "time-labels"?: WideLabel[]; "priority-labels"?: WideLabel[]; "auto-pay-mode"?: boolean; - "analytics-mode"?: boolean; - "incentive-mode"?: boolean; - "max-concurrent-bounties"?: number; + "promotion-comment"?: string; + "disable-analytics"?: boolean; + "comment-incentives"?: boolean; + "max-concurrent-assigns"?: number; "comment-element-pricing"?: Record; + "default-labels"?: string[]; + "register-wallet-with-verification"?: boolean; } export type WideRepoConfig = WideConfig; @@ -80,7 +86,7 @@ export const parseYAML = (data?: string): WideConfig | undefined => { export const getDefaultConfig = (): WideRepoConfig => { const defaultConfig = readFileSync(`${__dirname}/../../ubiquibot-config-default.yml`, "utf8"); - return parseYAML(defaultConfig)!; + return parseYAML(defaultConfig) as WideRepoConfig; }; export const getPrivateKey = async (cipherText: string): Promise => { @@ -134,17 +140,20 @@ export const getWideConfig = async (context: Context) => { const configs = { parsedRepo, parsedOrg, parsedDefault }; const configData = { - chainId: getChainId(configs), + networkId: getNetworkId(configs), privateKey: privateKeyDecrypted ?? "", baseMultiplier: getBaseMultiplier(configs), issueCreatorMultiplier: getCreatorMultiplier(configs), timeLabels: getTimeLabels(configs), priorityLabels: getPriorityLabels(configs), autoPayMode: getAutoPayMode(configs), - analyticsMode: getAnalyticsMode(configs), + disableAnalytics: getAnalyticsMode(configs), bountyHunterMax: getBountyHunterMax(configs), incentiveMode: getIncentiveMode(configs), commentElementPricing: getCommentItemPrice(configs), + defaultLabels: getDefaultLabels(configs), + promotionComment: getPromotionComment(configs), + registerWalletWithVerification: getRegisterWalletWithVerification(configs), }; return configData; diff --git a/supabase/README.md b/supabase/README.md index 636cfd57c..bdfa8ab90 100644 --- a/supabase/README.md +++ b/supabase/README.md @@ -2,20 +2,18 @@ [Supabase](https://supabase.com/) is used to store bounty hunters profiles and bounties information. - - ### How to setup supabase project locally 1. To get started with supabase, you have to create a project at [Supabase](https://supabase.com/). -Once you create a project, please put both variables into `.env` file. + Once you create a project, please put both variables into `.env` file. ``` SUPABASE_URL=XXX SUPABASE_KEY=XXX ``` -2. -[The Supabase CLI](https://supabase.com/docs/guides/resources/supabase-cli) available as a node package through the dev dependencies provides tools to develop your project locally and deploy to the Supabase Platform. -Most common useful commands are + +2. [The Supabase CLI](https://supabase.com/docs/guides/resources/supabase-cli) available as a node package through the dev dependencies provides tools to develop your project locally and deploy to the Supabase Platform. + Most common useful commands are - Run Supabase locally @@ -51,7 +49,7 @@ yarn supabase gen types ```sh yarn supabase link -p PASSWORD --project-ref PROJECT_REF -``` +``` For more information about arguments, please go through [here](https://supabase.com/docs/reference/cli/supabase-link) @@ -59,4 +57,4 @@ For more information about arguments, please go through [here](https://supabase. - `supabase migration new MIGRATION_NAME`: It will create a migration file in supabase/migrations folder. - `supabase db push -p PASSWORD`: Update database schema on supabase platform -- `supabase gen types typescript > src/adapters/supabase/types/database.types.ts --linked`: Generate typescript types from the supabase project linked +- `supabase gen types typescript > src/adapters/supabase/types/database.ts --linked`: Generate typescript types from the supabase project linked diff --git a/ubiquibot-config-default.yml b/ubiquibot-config-default.yml index fe7100f6f..5b0c70ac9 100644 --- a/ubiquibot-config-default.yml +++ b/ubiquibot-config-default.yml @@ -1,6 +1,6 @@ --- -chain-id: 1 -base-multiplier: 1000 +evm-network-id: 1 +price-multiplier: 1000 issue-creator-multiplier: 2000 time-labels: - name: "Time: <1 Hour" @@ -30,12 +30,15 @@ priority-labels: - name: "Priority: 4 (Emergency)" weight: 5 auto-pay-mode: true -analytics-mode: true -incentive-mode: false -max-concurrent-bounties: 2 +disable-analytics: true +comment-incentives: false +max-concurrent-assigns: 2 comment-element-pricing: text: 0.1 link: 0.5 list: 0.5 code: 5 - image: 5 \ No newline at end of file + image: 5 +default-labels: [] +promotion-comment: "\n
      If you enjoy the DevPool experience, please follow Ubiquity on GitHub and star this repo to show your support. It helps a lot!
      " +register-wallet-with-verification: false