diff --git a/.env.example b/.env.example index f96ce5b63..213356e0f 100644 --- a/.env.example +++ b/.env.example @@ -13,3 +13,10 @@ ANALYTICS_MODE= # Use `trace` to get verbose logging or `info` to show less LOG_LEVEL=debug LOGDNA_INGESTION_KEY= +OPENAI_API_HOST=https://api.openai.com +OPENAI_API_KEY= +CHATGPT_USER_PROMPT_FOR_IMPORTANT_WORDS="I need your help to find important words (e.g. unique adjectives) from github issue below and I want to parse them easily so please separate them using #(No other contexts needed). Please separate the words by # so I can parse them easily. Please answer simply as I only need the important words. Here is the issue content.\n" +CHATGPT_USER_PROMPT_FOR_MEASURE_SIMILARITY='I have two github issues and I need to measure the possibility of the 2 issues are the same content (No other contents needed and give me only the number in %).\n Give me in number format and add % after the number.\nDo not tell other things since I only need the number (e.g. 85%). Here are two issues:\n 1. "%first%"\n2. "%second%"' +SIMILARITY_THRESHOLD=80 +MEASURE_SIMILARITY_AI_TEMPERATURE=0 +IMPORTANT_WORDS_AI_TEMPERATURE=0 diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 8b1378917..4a5a1e837 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1 +1 @@ - +* @0xcodercrane diff --git a/.github/ubiquibot-config.yml b/.github/ubiquibot-config.yml index f79f61652..5d5a81c05 100644 --- a/.github/ubiquibot-config.yml +++ b/.github/ubiquibot-config.yml @@ -1,39 +1,5 @@ ---- -evm-network-id: 100 price-multiplier: 1.5 -time-labels: - - name: "Time: <1 Hour" - weight: 0.125 - value: 3600 - - name: "Time: <1 Day" - weight: 1 - value: 86400 - - name: "Time: <1 Week" - weight: 2 - value: 604800 - - name: "Time: <2 Weeks" - weight: 3 - value: 1209600 - - name: "Time: <1 Month" - weight: 4 - value: 2592000 -priority-labels: - - name: "Priority: 0 (Normal)" - weight: 1 - - name: "Priority: 1 (Medium)" - weight: 2 - - name: "Priority: 2 (High)" - weight: 3 - - name: "Priority: 3 (Urgent)" - weight: 4 - - name: "Priority: 4 (Emergency)" - weight: 5 + default-labels: - "Time: <1 Hour" - - "Priority: 0 (Normal)" - - "Test" -auto-pay-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 + - "Priority: 1 (Normal)" diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index c790059a3..fa5fedab5 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -81,4 +81,4 @@ jobs: X25519_PRIVATE_KEY: 'QCDb30UHUkwJAGhLWC-R2N0PiEbd4vQY6qH2Wloybyo' FOLLOW_UP_TIME: '4 days' DISQUALIFY_TIME: '7 days' - run: yarn start:serverless \ No newline at end of file + run: yarn start:serverless diff --git a/.github/workflows/deploy-logger-page.yml b/.github/workflows/deploy-logger-page.yml new file mode 100644 index 000000000..d827c708f --- /dev/null +++ b/.github/workflows/deploy-logger-page.yml @@ -0,0 +1,37 @@ +name: Deploy Log Web App to Cloudflare Worker + +on: + workflow_run: + workflows: ["Conventional Commits"] + types: + - completed + +jobs: + publish: + runs-on: ubuntu-latest + permissions: + contents: read + deployments: write + name: Deploy to Cloudflare Worker + + steps: + - name: Checkout repository + uses: actions/checkout@v3 + + - name: Setup Node + uses: actions/setup-node@v3 + with: + node-version: "20.3.0" + + - name: Build TypeScript + run: cd ./log-app && npm install && npm run build + + - name: Publish to Cloudflare Pages + uses: cloudflare/pages-action@v1 + with: + apiToken: ${{ secrets.CLOUDFLARE_API_TOKEN }} + accountId: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }} + projectName: ${{ secrets.CLOUDFLARE_PROJECT_NAME }} + directory: ${{ secrets.LOGGER_WEB_APP_DIRECTORY }} + # Enable Wrangler v3 + wranglerVersion: "3" diff --git a/.github/workflows/kebab-case.yml b/.github/workflows/kebab-case.yml index 4fdb2ec11..892480a87 100644 --- a/.github/workflows/kebab-case.yml +++ b/.github/workflows/kebab-case.yml @@ -24,6 +24,7 @@ jobs: "\.sql$" "\.md$" "\.d.ts$" + "^\.\/\log-app" ) while read -r file; do basefile=$(basename "$file") diff --git a/.github/workflows/update-config.yml b/.github/workflows/update-config.yml new file mode 100644 index 000000000..c93d04bfb --- /dev/null +++ b/.github/workflows/update-config.yml @@ -0,0 +1,98 @@ +name: Pull Request Action +permissions: write-all +on: + workflow_dispatch: + +env: + GH_TOKEN: ${{ secrets.ADD_TO_PROJECT_PAT }} + +jobs: + build: + runs-on: ubuntu-latest + steps: + + - name: Check out repository + uses: actions/checkout@v3 + + - name: Install jq and yq + run: | + sudo apt-get -y install jq + sudo wget https://github.com/mikefarah/yq/releases/latest/download/yq_linux_amd64 -O /usr/bin/yq + sudo chmod +x /usr/bin/yq + + - name: Get UbiquiBot Token + uses: tibdex/github-app-token@v1.7.0 + id: get_installation_token + with: + app_id: ${{ secrets.UBIQUITY_BOUNTY_BOT_APP_ID }} + private_key: ${{ secrets.UBIQUITY_BOUNTY_BOT_PRIVATE_KEY }} + + - name: Update Config Params and Create Pull Requests + run: | + urls=$(curl -sSL https://raw.githubusercontent.com/ubiquity/devpool-directory/development/projects.json | jq -r '.urls[]') + + for url in $urls + do + repoName=$(basename $url) + ownerName=$(echo $url | awk -F/ '{print $(NF-1)}') + + git clone $url $repoName + cd $repoName + defaultBranch=$(git branch --show-current) + + # make a branch to update config # + git branch update + git checkout update + + curl -sSL https://raw.githubusercontent.com/ubiquity/ubiquibot/development/ubiquibot-config-default.json > default.json + declare -A param_mapping=( + ["evm-network-id"]="network-id chain-id" + ["price-multiplier"]="base-multiplier" + #add more configs as needed + ) + + ### update configs ### + # Iterate over the mapping and perform updates using sed + for new_param in "${!param_mapping[@]}" + do + old_params="${param_mapping[$new_param]}" + for old_param in $old_params + do + # only update param if the old ones exist + exist_old_param=$(yq "has(\"$old_param\")" .github/ubiquibot-config.yml) + if $exist_old_param; then + yq ".$new_param = .$old_param | del(.$old_param)" .github/ubiquibot-config.yml > temp.yml + mv temp.yml .github/ubiquibot-config.yml + fi + done + # if new param still doesent exist add default from ubiquibot-config-default.json + exist_new_param=$(yq "has(\"$new_param\")" .github/ubiquibot-config.yml) + if ! $exist_new_param; then + echo adding + def_val=$(jq -r ".[\"$new_param\"]" ubiquibot-config-default.json) + yq ".$new_param=$def_val" .github/ubiquibot-config.yml > temp.yml + mv temp.yml .github/ubiquibot-config.yml + fi + done + + git config user.email "113181824+UbiquiBot[bot]@users.noreply.github.com" + git config user.name "UbiquiBot[bot]" + + git add .github/ubiquibot-config.yml + git commit -m "build: use latest ubiquibot config setup" + git remote set-url origin https://${{ secrets.ADD_TO_PROJECT_PAT }}@github.com/$ownerName/$repoName.git + git push -f origin update + + curl -L \ + -X POST \ + -H "Accept: application/vnd.github+json" \ + -H "Authorization: Bearer ${{ steps.get_installation_token.outputs.token }}"\ + -H "X-GitHub-Api-Version: 2022-11-28" \ + https://api.github.com/repos/$ownerName/$repoName/pulls \ + -d '{ + "title": "build: use latest ubiquibot config setup", + "base": "'"$defaultBranch"'", + "head": "update" + }' + cd .. + done \ No newline at end of file diff --git a/README.md b/README.md index 0311933d0..4b38ed11d 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ Ubiquity DAO's GitHub Bot to automate DevPool management. git clone https://github.com/ubiquity/ubiquibot.git cd ubiquibot yarn -yarn tsc +yarn build yarn start:watch ``` @@ -23,6 +23,15 @@ yarn start:watch - `LOGDNA_INGESTION_KEY`: Get it from [Memzo](https://app.mezmo.com/) by creating an account, adding an organization, and copying the ingestion key on the next screen. - `FOLLOWUP_TIME`: (optional) Set a custom follow-up time (default: 4 days). - `DISQUALIFY_TIME`: (optional) Set a custom disqualify time (default: 7 days). +- `OPENAI_API_HOST`: (optional) Set OpenAI host url (default: https://api.openai.com). +- `OPENAI_API_KEY`: Set OpenAI key. +- `CHATGPT_USER_PROMPT_FOR_IMPORTANT_WORDS`: (optional) Set a custom user prompt for finding important words +(default: "I need your help to find important words (e.g. unique adjectives) from github issue below and I want to parse them easily so please separate them using #(No other contexts needed). Please separate the words by # so I can parse them easily. Please answer simply as I only need the important words. Here is the issue content.\n"). +- `CHATGPT_USER_PROMPT_FOR_MEASURE_SIMILARITY`: (optional) Set a custom user prompt for measuring similarity +(default: 'I have two github issues and I need to measure the possibility of the 2 issues are the same content (No other contents needed and give me only the number in %).\n Give me in number format and add % after the number.\nDo not tell other things since I only need the number (e.g. 85%). Here are two issues:\n 1. "%first%"\n2. "%second%"'). +- `SIMILARITY_THRESHOLD`: (optional) Set similarity threshold (default: 80). +- `MEASURE_SIMILARITY_AI_TEMPERATURE`: (optional) Set ChatGPT temperature for measuring similarity (default: 0). +- `IMPORTANT_WORDS_AI_TEMPERATURE`: (optional) Set ChatGPT temperature for finding important words (default: 0). `APP_ID` and `PRIVATE_KEY` are [here](https://t.me/c/1588400061/1627) for internal developers to use. If you are an external developer, `APP_ID`and `PRIVATE_KEY` are automatically generated when you install the app on your repository. @@ -45,38 +54,55 @@ To test the bot, you can: 1. Create a new issue 2. Add a time label, ex: `Time: <1 Day` -3. At this point the bot should add a price label. +3. Add a priority label, ex: `Priority: 0 (Normal)` +4. 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. +`evm-network-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` +`price-multiplier` is a base number that will be used to calculate bounty price based on the following formula: `price = price-multiplier * time-label-weight * priority-label-weight * 100` `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 + +`command-settings` are setting to enable or disable a command + +- `name` is the name of the command +- `enabled` is a `true` or `false` value to enable or disable a command `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. +`assistive-pricing` to create a new pricing label if it doesn't exist. Can be `true` or `false`. + +`disable-analytics` can be `true` or `false` that disables or enables weekly analytics collection by Ubiquity. + +`payment-permit-max-price` sets the max amount for automatic payout of bounties when the issue is closed. + +`comment-incentives` can be `true` or `false` that enable or disable comment incentives. These are payments generated for comments in the issue by contributors, excluding the assignee. + +`issue-creator-multiplier` is a number that defines a base multiplier for calculating incentive 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 add 0.1 -`analytics-mode` can be `true` or `false` that enables or disables weekly analytics collection by Ubiquity. +`incentives` defines incentive rewards: -`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. +- `comment` defines comment rewards: + - `elements` defines reward value for HTML elements such as `p`, `img`, `a`. + - `totals`: + - `word` defines reward for each word in the comment -`issue-creator-multiplier` is a number that defines a base multiplier for calculating incentive reward for the creator of the issue. +`max-concurrent-assigns` is the maximum number of bounties that can be assigned to a bounty hunter at once. This excludes bounties with delayed or approved pull request reviews. -`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 +`register-wallet-with-verification` can be `true` or `false`. If enabled, it requires a signed message to set wallet address. This prevents users from setting wallet address from centralized exchanges, which would make payments impossible to claim. -`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. +`promotion-comment` is a message that is appended to the payment permit comment. ## How to run locally @@ -108,7 +134,7 @@ DISQUALIFY_TIME="7 days" // 7 days 4. `yarn install` 5. Open 2 terminal instances: - - in one instance run `yarn tsc --watch` (compiles the Typescript code) + - in one instance run `yarn build --watch` (compiles the Typescript code) - in another instance run `yarn start:watch` (runs the bot locally) 6. Open `localhost:3000` and follow instructions to add the bot to one of your repositories. @@ -119,7 +145,8 @@ You can, for example: 1. Create a new issue 2. Add a time label, ex: `Time: <1 Day` -3. At this point the bot should add a price label, you should see event logs in one of your opened terminals +3. Add a priority label, ex: `Priority: 0 (Normal)` +4. At this point the bot should add a price label, you should see event logs in one of your opened terminals ## How it works @@ -135,13 +162,31 @@ When using as a github app the flow is the following: 4. Event details are sent to your deployed bot instance (to a webhook URL that was set in github app's settings) 5. The bot handles the event +## Payments Permits in a local instance + +For payment to work in your local instance, ubiquibot must be set up in a Github organization. It will not work for a ubiquibot instance set up in a personal account. Once, you have an ubiquibot instance working in an organization, follow the steps given below: + +1. Create a new private repository in your Github organization with name `ubiquibot-config` +2. Add your ubiquibot app to `ubiquibot-config` repository. +3. Create a file `.github/ubiquibot-config.yml` in it. Fill the file with contents from [this file](https://github.com/ubiquity/ubiquibot/blob/development/.github/ubiquibot-config.yml). +4. Go to https://pay.ubq.fi/keygen and generate X25519 public/private key pair. Fill private key of your wallet's address in `PLAIN_TEXT` field and click `Encrypt`. +5. Copy the `CIPHER_TEXT` and append it to your repo `ubiquibot-config/.github/ubiquibot-config.yml` as + + `private-key-encrypted: "PASTE_YOUR_CIPHER_TEXT_HERE"` + +6. Copy the `X25519_PRIVATE_KEY` and append it in your local ubiquibot repository `.env` file as + + `X25519_PRIVATE_KEY=PASTE_YOUR_X25519_PRIVATE_KEY_HERE` + ## How to QA any additions to the bot -1. Fork the ubiquibot repo and install the [ubiquibot-qa app](https://github.com/apps/ubiquibot-qa) on the forked repository. -2. Enable github action running on the forked repo and allow `issues` on the settings tab. -3. Create a [QA issue](https://github.com/ubiquibot/staging/issues/21) similar to this where you show the feature working in the forked repo -4. Describe carefully the steps taken to get the feature working, this way our team can easily verify -5. Link that QA issue to the pull request as indicated on the template before requesting a review +Make sure you have your local instance of ubiquibot running. + +1. Fork the ubiquibot repo and add your local instance of ubiquibot to the forked repository. +2. Enable Github action running on the forked repo and allow `issues` on the settings tab. +3. Create a [QA issue](https://github.com/ubiquibot/staging/issues/21) similar to this where you show the feature working in the forked repo. +4. Describe carefully the steps taken to get the feature working, this way our team can easily verify. +5. Link that QA issue to the pull request as indicated on the template before requesting a review. ## How to create a new release @@ -178,3 +223,43 @@ Bounty bot is built using the [probot](https://probot.github.io/) framework so i | ├── utils A set of utility functions + +## Default Config Notes (`ubiquibot-config-default.json`) + +We can't use a `jsonc` file due to limitations with Netlify. Here is a snippet of some values with notes next to them. + +```jsonc +{ + "payment-permit-max-price": 9007199254740991, // Number.MAX_SAFE_INTEGER + "max-concurrent-assigns": 9007199254740991, // Number.MAX_SAFE_INTEGER + "comment-element-pricing": { + /* https://github.com/syntax-tree/mdast#nodes */ + "strong": 0 // Also includes italics, unfortunately https://github.com/syntax-tree/mdast#strong + /* https://github.com/syntax-tree/mdast#gfm */ + } +} +``` + +## Supabase Cron Job (`logs-cleaner`) + +##### Dashboard > Project > Database > Extensions + +> Search `PG_CRON` and Enable it. + + +##### Dashboard > Project > SQL Editor + +```sql +-- Runs everyday at 03:00 AM to cleanup logs that are older than a week +-- Use the cron time format to modify the trigger time if necessary +select + cron.schedule ( + 'logs-cleaner', -- Job name + '0 3 * * *', -- Everyday at 03:00 AM + $$DELETE FROM logs WHERE timestamp < now() - INTERVAL '1 week'$$ + ); + + +-- Cancel the cron job +select cron.unschedule('logs-cleaner'); +``` diff --git a/log-app/.gitignore b/log-app/.gitignore new file mode 100644 index 000000000..9df249787 --- /dev/null +++ b/log-app/.gitignore @@ -0,0 +1,2 @@ +# Ignore JavaScript files +*.js \ No newline at end of file diff --git a/log-app/_worker.ts b/log-app/_worker.ts new file mode 100644 index 000000000..526cedbb3 --- /dev/null +++ b/log-app/_worker.ts @@ -0,0 +1,9 @@ +import { Env } from "./types/global"; + +export default { + async fetch(request: Request, env: Env) { + // Otherwise, serve the static assets. + // Without this, the Worker will error and no assets will be served. + return env.ASSETS.fetch(request); + }, +}; diff --git a/log-app/index.html b/log-app/index.html new file mode 100644 index 000000000..04ab5014d --- /dev/null +++ b/log-app/index.html @@ -0,0 +1,44 @@ + + + + + + + Log Viewer + + + + +
+ + +
+ + + + + + + + + + +
MessageLevelTimestampComment URL
+ + + + + + \ No newline at end of file diff --git a/log-app/package.json b/log-app/package.json new file mode 100644 index 000000000..f70c61f60 --- /dev/null +++ b/log-app/package.json @@ -0,0 +1,16 @@ +{ + "name": "cf-pages-functions", + "version": "0.0.0", + "scripts": { + "build": "tsc && tsx ./scripts/esbuild.ts" + }, + "devDependencies": { + "@cloudflare/workers-types": "^3.2.0", + "@supabase/supabase-js": "^2.32.0", + "typescript": "^4.3.2" + }, + "dependencies": { + "esbuild": "^0.19.2", + "tsx": "^3.12.7" + } +} diff --git a/log-app/scripts/constants/index.ts b/log-app/scripts/constants/index.ts new file mode 100644 index 000000000..dbda210ab --- /dev/null +++ b/log-app/scripts/constants/index.ts @@ -0,0 +1,3 @@ +export const SUPABASE_URL = "https://qzfjblxdroqrmlheoajw.supabase.co"; +export const SUPABASE_KEY = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6InF6ZmpibHhkcm9xcm1saGVvYWp3Iiwicm9sZSI6ImFub24iLCJpYXQiOjE2ODA4NTMwMzMsImV4cCI6MTk5NjQyOTAzM30.BM_qkpX-egNdiMc0PDO_34bIaXHB7ewnr2k4U2hGFMM"; diff --git a/log-app/scripts/esbuild-config.ts b/log-app/scripts/esbuild-config.ts new file mode 100644 index 000000000..9a0c2f714 --- /dev/null +++ b/log-app/scripts/esbuild-config.ts @@ -0,0 +1,19 @@ +import esbuild from "esbuild"; +const typescriptEntries = ["scripts/helpers/utils.ts", "scripts/index.ts", "scripts/constants/index.ts"]; +export const entries = [...typescriptEntries]; + +export let esBuildContext = { + sourcemap: false, + entryPoints: entries, + bundle: true, + minify: false, + loader: { + ".png": "dataurl", + ".woff": "dataurl", + ".woff2": "dataurl", + ".eot": "dataurl", + ".ttf": "dataurl", + ".svg": "dataurl", + }, + outdir: "scripts", +} as esbuild.BuildOptions; diff --git a/log-app/scripts/esbuild.ts b/log-app/scripts/esbuild.ts new file mode 100644 index 000000000..bd358a938 --- /dev/null +++ b/log-app/scripts/esbuild.ts @@ -0,0 +1,3 @@ +import esbuild from "esbuild"; +import { esBuildContext } from "./esbuild-config"; +esbuild.build(esBuildContext); diff --git a/log-app/scripts/helpers/utils.ts b/log-app/scripts/helpers/utils.ts new file mode 100644 index 000000000..31ce87681 --- /dev/null +++ b/log-app/scripts/helpers/utils.ts @@ -0,0 +1,66 @@ +export enum Level { + ERROR = "error", + WARN = "warn", + INFO = "info", + HTTP = "http", + VERBOSE = "verbose", + DEBUG = "debug", + SILLY = "silly", +} + +export const createGitHubCommentURL = (orgName: string, repoName: string, issueNumber: number, commentId: number) => { + return `https://github.com/${orgName}/${repoName}/issues/${issueNumber}#issuecomment-${commentId}`; +}; + +export const isValidJson = (jsonString) => { + try { + JSON.parse(jsonString); + return true; + } catch (error) { + return false; + } +}; + +export const generateRandomId = (length) => { + return [...Array(length)].map(() => Math.random().toString(36)[2]).join(""); +}; + +export const containsValidJson = (message: string): [boolean, string, string] => { + const jsonMatches = message.match(/\{.*\}/g); // Find JSON-like substrings + if (!jsonMatches) { + return [false, "", ""]; + } + + for (const match of jsonMatches) { + if (isValidJson(match)) { + const braceIndex = message.indexOf("{"); + if (braceIndex !== -1) { + return [true, match, message.substring(0, braceIndex)]; + } + return [true, match, ""]; + } + } + + return [false, "", ""]; +}; + +export const getLevelString = (level: number) => { + switch (level) { + case 0: + return Level.ERROR; + case 1: + return Level.WARN; + case 2: + return Level.INFO; + case 3: + return Level.HTTP; + case 4: + return Level.VERBOSE; + case 5: + return Level.DEBUG; + case 6: + return Level.SILLY; + default: + return -1; // Invalid level + } +}; diff --git a/log-app/scripts/index.ts b/log-app/scripts/index.ts new file mode 100644 index 000000000..a0d5c1817 --- /dev/null +++ b/log-app/scripts/index.ts @@ -0,0 +1,99 @@ +import { containsValidJson, createGitHubCommentURL, generateRandomId, getLevelString } from "./helpers/utils"; +import { Logs } from "./types/log"; +import { createClient } from "@supabase/supabase-js"; +import { SUPABASE_URL, SUPABASE_KEY } from "./constants/index"; + +const filterSelect = document.getElementById("filter") as unknown as HTMLSelectElement; +const clearButton = document.getElementById("clear") as HTMLButtonElement; +const logBody = document.getElementById("log-body") as HTMLDivElement; + +const jsonModal = document.getElementById("json-modal") as HTMLDivElement; +const closeModalButton = document.getElementById("close-modal") as HTMLButtonElement; +const jsonContent = document.getElementById("json-content") as HTMLDivElement; + +const openJsonModal = (validJson) => { + jsonContent.textContent = validJson; + + jsonModal.style.display = "flex"; +}; + +const updateLogTable = () => { + const selectedFilter = filterSelect.value; + const filteredLogs = selectedFilter === "all" ? logs : logs.filter((log) => getLevelString(log.level) === selectedFilter); + + logBody.innerHTML = ""; + filteredLogs.forEach((log) => { + const classId = generateRandomId(10); + const commentUrl = createGitHubCommentURL(log.org_name, log.repo_name, log.issue_number, log.comment_id); + const row = document.createElement("tr"); + const [validJson, match, beforeText] = containsValidJson(log.log_message); + row.innerHTML = ` + ${ + validJson + ? `${beforeText} - ` + : `${log.log_message}` + } + ${getLevelString(log.level)} + ${log.timestamp} + Comment - ${log.comment_id} + `; + logBody.appendChild(row); + let logCell = document.getElementsByClassName("log-cell"); + if (validJson) { + // show modal button for valid json row + const showMoreButton = document.getElementById(`button_${classId}`) as HTMLButtonElement; + showMoreButton.addEventListener("click", () => { + if (validJson) { + openJsonModal(JSON.stringify(JSON.parse(match), null, 2)); // properly formatted json + } + }); + } + // scroll to last added data + logCell[logCell.length - 1].scrollIntoView(); + }); +}; + +let logs: Logs[] = []; + +const supabaseClient = createClient(SUPABASE_URL, SUPABASE_KEY); + +const channel = supabaseClient + .channel("table-db-changes") + .on( + "postgres_changes", + { + event: "INSERT", + schema: "public", + table: "logs", + }, + (payload) => handlePayload(payload) + ) + .subscribe(); + +const handlePayload = (logEntry) => { + if (logEntry?.eventType !== "INSERT") return; + logs.push(logEntry.new); + updateLogTable(); +}; + +filterSelect.addEventListener("change", () => { + updateLogTable(); +}); + +clearButton.addEventListener("click", () => { + logs = []; + updateLogTable(); +}); + +closeModalButton.addEventListener("click", () => { + jsonModal.style.display = "none"; +}); + +window.addEventListener("click", (event) => { + if (event.target === jsonModal) { + jsonModal.style.display = "none"; + } +}); + +// Initial update +updateLogTable(); diff --git a/log-app/scripts/tsconfig.json b/log-app/scripts/tsconfig.json new file mode 100644 index 000000000..018499861 --- /dev/null +++ b/log-app/scripts/tsconfig.json @@ -0,0 +1,15 @@ +{ + "compilerOptions": { + "useDefineForClassFields": true, + "allowJs": false, + "esModuleInterop": true, + "module": "CommonJS", + "allowSyntheticDefaultImports": true, + "strict": false, + "forceConsistentCasingInFileNames": true, + "target": "esnext" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */, + "lib": ["esnext", "dom", "dom.iterable"] /* Specify a set of bundled library declaration files that describe the target runtime environment. */ + }, + "include": ["./helpers", "./constants", "./index.ts"], + "exclude": ["./esbuild.ts", "./esbuild-config.ts"] +} diff --git a/log-app/scripts/types/log.ts b/log-app/scripts/types/log.ts new file mode 100644 index 000000000..ebd2b43a2 --- /dev/null +++ b/log-app/scripts/types/log.ts @@ -0,0 +1,9 @@ +export type Logs = { + timestamp: string; + log_message: string; + level: number; + repo_name: string; + org_name: string; + comment_id: number; + issue_number: number; +}; diff --git a/log-app/styles/index.css b/log-app/styles/index.css new file mode 100644 index 000000000..7ba057db5 --- /dev/null +++ b/log-app/styles/index.css @@ -0,0 +1,156 @@ +body { + font-family: Arial, sans-serif; +} + +.log-container { + margin: 20px auto; + width: 95%; +} + +.log-cell { + border: 1px solid #ccc; + padding: 8px; + text-align: left; + max-width: 476px; + /* Adjust the maximum width as needed */ + overflow: auto; + /* Allow scrolling if the content is too long */ + white-space: pre-wrap; +} + +#log-table { + margin: 20px auto; + width: 95%; + border-collapse: collapse; + border: 1px solid #ccc; + margin-top: 20px; + table-layout: fixed; +} + +#log-table th, +#log-table td { + border: 1px solid #ccc; + padding: 10px; + text-align: left; + color: black; +} + +#log-table th { + background-color: #f5f5f5; + font-weight: bold; +} + +#log-body { + max-height: 400px; + overflow-y: auto; +} + +table { + border-collapse: collapse; + width: 600px; +} + +td, +th { + padding: 10px; + text-align: left; + margin: 0; + color: black; +} + +tbody tr:nth-child(2n) { + background-color: #eee; +} + +th { + position: sticky; + top: 0; + background-color: #333; + color: white; +} + +select, +button { + margin-right: 10px; +} + +#filter { + font-family: Arial, sans-serif; + padding: 8px 24px; + border: 1px solid #ccc; + border-radius: 4px; + background-color: white; + cursor: pointer; + transition: border-color 0.3s; +} + +#filter:hover { + border-color: #999; +} + +/* Styling for button element */ +#clear { + font-family: Arial, sans-serif; + padding: 8px 12px; + background-color: #007bff; + border: none; + border-radius: 4px; + color: white; + cursor: pointer; + transition: background-color 0.3s; +} + +#clear:hover { + background-color: #0056b3; +} + +/* Modal styles */ +.modal { + display: none; + position: fixed; + z-index: 1; + left: 0; + top: 0; + width: 100%; + height: 100%; + background-color: rgba(0, 0, 0, 0.4); + align-items: center; + justify-content: center; + /* Center horizontally and vertically */ +} + +.modal-content { + background-color: white; + border: 1px solid #888; + width: 80%; + max-width: 600px; + height: 75vh; + padding: 20px; + text-align: left; + border-radius: 8px; + box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1); +} + +.close { + color: #aaa; + float: right; + font-size: 28px; + font-weight: bold; +} + +.close:hover, +.close:focus { + color: black; + text-decoration: none; + cursor: pointer; +} + +.json-content { + overflow: auto; + height: 70vh; + padding: 10px; + background-color: #f5f5f5; + border: 1px solid #ddd; + border-radius: 4px; + white-space: pre-wrap; +} \ No newline at end of file diff --git a/log-app/tsconfig.json b/log-app/tsconfig.json new file mode 100644 index 000000000..d572b4033 --- /dev/null +++ b/log-app/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ESNext", + "useDefineForClassFields": true, + "lib": ["ESNext"], + "allowJs": false, + "skipLibCheck": true, + "esModuleInterop": false, + "allowSyntheticDefaultImports": true, + "strict": true, + "forceConsistentCasingInFileNames": true, + "module": "ESNext", + "moduleResolution": "Node", + "types": ["@cloudflare/workers-types", "node"], + "outDir": "" + }, + "include": ["./_worker.ts", "./src", "./types"] +} diff --git a/log-app/types/global.ts b/log-app/types/global.ts new file mode 100644 index 000000000..a664d7f42 --- /dev/null +++ b/log-app/types/global.ts @@ -0,0 +1,5 @@ +export interface Env { + ASSETS: Fetcher; + SUPABASE_KEY: string; + SUPABASE_URL: string; +} diff --git a/log-app/yarn.lock b/log-app/yarn.lock new file mode 100644 index 000000000..7d3f90eda --- /dev/null +++ b/log-app/yarn.lock @@ -0,0 +1,573 @@ +# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY. +# yarn lockfile v1 + + +"@cloudflare/workers-types@^3.2.0": + version "3.19.0" + resolved "https://registry.yarnpkg.com/@cloudflare/workers-types/-/workers-types-3.19.0.tgz#f3791b80b23f7cf0072f2e67e98aa37801204b6c" + integrity sha512-0FRcsz7Ea3jT+gc5gKPIYciykm1bbAaTpygdzpCwGt0RL+V83zWnYN30NWDW4rIHj/FHtz+MIuBKS61C8l7AzQ== + +"@esbuild-kit/cjs-loader@^2.4.2": + version "2.4.2" + resolved "https://registry.yarnpkg.com/@esbuild-kit/cjs-loader/-/cjs-loader-2.4.2.tgz#cb4dde00fbf744a68c4f20162ea15a8242d0fa54" + integrity sha512-BDXFbYOJzT/NBEtp71cvsrGPwGAMGRB/349rwKuoxNSiKjPraNNnlK6MIIabViCjqZugu6j+xeMDlEkWdHHJSg== + dependencies: + "@esbuild-kit/core-utils" "^3.0.0" + get-tsconfig "^4.4.0" + +"@esbuild-kit/core-utils@^3.0.0": + version "3.1.0" + resolved "https://registry.yarnpkg.com/@esbuild-kit/core-utils/-/core-utils-3.1.0.tgz#49945d533dbd5e1b7620aa0fc522c15e6ec089c5" + integrity sha512-Uuk8RpCg/7fdHSceR1M6XbSZFSuMrxcePFuGgyvsBn+u339dk5OeL4jv2EojwTN2st/unJGsVm4qHWjWNmJ/tw== + dependencies: + esbuild "~0.17.6" + source-map-support "^0.5.21" + +"@esbuild-kit/esm-loader@^2.5.5": + version "2.5.5" + resolved "https://registry.yarnpkg.com/@esbuild-kit/esm-loader/-/esm-loader-2.5.5.tgz#b82da14fcee3fc1d219869756c06f43f67d1ca71" + integrity sha512-Qwfvj/qoPbClxCRNuac1Du01r9gvNOT+pMYtJDapfB1eoGN1YlJ1BixLyL9WVENRx5RXgNLdfYdx/CuswlGhMw== + dependencies: + "@esbuild-kit/core-utils" "^3.0.0" + get-tsconfig "^4.4.0" + +"@esbuild/android-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.17.19.tgz#bafb75234a5d3d1b690e7c2956a599345e84a2fd" + integrity sha512-KBMWvEZooR7+kzY0BtbTQn0OAYY7CsiydT63pVEaPtVYF0hXbUaOyZog37DKxK7NF3XacBJOpYT4adIJh+avxA== + +"@esbuild/android-arm64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm64/-/android-arm64-0.19.2.tgz#bc35990f412a749e948b792825eef7df0ce0e073" + integrity sha512-lsB65vAbe90I/Qe10OjkmrdxSX4UJDjosDgb8sZUKcg3oefEuW2OT2Vozz8ef7wrJbMcmhvCC+hciF8jY/uAkw== + +"@esbuild/android-arm@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.17.19.tgz#5898f7832c2298bc7d0ab53701c57beb74d78b4d" + integrity sha512-rIKddzqhmav7MSmoFCmDIb6e2W57geRsM94gV2l38fzhXMwq7hZoClug9USI2pFRGL06f4IOPHHpFNOkWieR8A== + +"@esbuild/android-arm@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-arm/-/android-arm-0.19.2.tgz#edd1c8f23ba353c197f5b0337123c58ff2a56999" + integrity sha512-tM8yLeYVe7pRyAu9VMi/Q7aunpLwD139EY1S99xbQkT4/q2qa6eA4ige/WJQYdJ8GBL1K33pPFhPfPdJ/WzT8Q== + +"@esbuild/android-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.17.19.tgz#658368ef92067866d95fb268719f98f363d13ae1" + integrity sha512-uUTTc4xGNDT7YSArp/zbtmbhO0uEEK9/ETW29Wk1thYUJBz3IVnvgEiEwEa9IeLyvnpKrWK64Utw2bgUmDveww== + +"@esbuild/android-x64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/android-x64/-/android-x64-0.19.2.tgz#2dcdd6e6f1f2d82ea1b746abd8da5b284960f35a" + integrity sha512-qK/TpmHt2M/Hg82WXHRc/W/2SGo/l1thtDHZWqFq7oi24AjZ4O/CpPSu6ZuYKFkEgmZlFoa7CooAyYmuvnaG8w== + +"@esbuild/darwin-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.17.19.tgz#584c34c5991b95d4d48d333300b1a4e2ff7be276" + integrity sha512-80wEoCfF/hFKM6WE1FyBHc9SfUblloAWx6FJkFWTWiCoht9Mc0ARGEM47e67W9rI09YoUxJL68WHfDRYEAvOhg== + +"@esbuild/darwin-arm64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-arm64/-/darwin-arm64-0.19.2.tgz#55b36bc06d76f5c243987c1f93a11a80d8fc3b26" + integrity sha512-Ora8JokrvrzEPEpZO18ZYXkH4asCdc1DLdcVy8TGf5eWtPO1Ie4WroEJzwI52ZGtpODy3+m0a2yEX9l+KUn0tA== + +"@esbuild/darwin-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.17.19.tgz#7751d236dfe6ce136cce343dce69f52d76b7f6cb" + integrity sha512-IJM4JJsLhRYr9xdtLytPLSH9k/oxR3boaUIYiHkAawtwNOXKE8KoU8tMvryogdcT8AU+Bflmh81Xn6Q0vTZbQw== + +"@esbuild/darwin-x64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/darwin-x64/-/darwin-x64-0.19.2.tgz#982524af33a6424a3b5cb44bbd52559623ad719c" + integrity sha512-tP+B5UuIbbFMj2hQaUr6EALlHOIOmlLM2FK7jeFBobPy2ERdohI4Ka6ZFjZ1ZYsrHE/hZimGuU90jusRE0pwDw== + +"@esbuild/freebsd-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.17.19.tgz#cacd171665dd1d500f45c167d50c6b7e539d5fd2" + integrity sha512-pBwbc7DufluUeGdjSU5Si+P3SoMF5DQ/F/UmTSb8HXO80ZEAJmrykPyzo1IfNbAoaqw48YRpv8shwd1NoI0jcQ== + +"@esbuild/freebsd-arm64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.19.2.tgz#8e478a0856645265fe79eac4b31b52193011ee06" + integrity sha512-YbPY2kc0acfzL1VPVK6EnAlig4f+l8xmq36OZkU0jzBVHcOTyQDhnKQaLzZudNJQyymd9OqQezeaBgkTGdTGeQ== + +"@esbuild/freebsd-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.17.19.tgz#0769456eee2a08b8d925d7c00b79e861cb3162e4" + integrity sha512-4lu+n8Wk0XlajEhbEffdy2xy53dpR06SlzvhGByyg36qJw6Kpfk7cp45DR/62aPH9mtJRmIyrXAS5UWBrJT6TQ== + +"@esbuild/freebsd-x64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/freebsd-x64/-/freebsd-x64-0.19.2.tgz#01b96604f2540db023c73809bb8ae6cd1692d6f3" + integrity sha512-nSO5uZT2clM6hosjWHAsS15hLrwCvIWx+b2e3lZ3MwbYSaXwvfO528OF+dLjas1g3bZonciivI8qKR/Hm7IWGw== + +"@esbuild/linux-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.17.19.tgz#38e162ecb723862c6be1c27d6389f48960b68edb" + integrity sha512-ct1Tg3WGwd3P+oZYqic+YZF4snNl2bsnMKRkb3ozHmnM0dGWuxcPTTntAF6bOP0Sp4x0PjSF+4uHQ1xvxfRKqg== + +"@esbuild/linux-arm64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm64/-/linux-arm64-0.19.2.tgz#7e5d2c7864c5c83ec789b59c77cd9c20d2594916" + integrity sha512-ig2P7GeG//zWlU0AggA3pV1h5gdix0MA3wgB+NsnBXViwiGgY77fuN9Wr5uoCrs2YzaYfogXgsWZbm+HGr09xg== + +"@esbuild/linux-arm@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.17.19.tgz#1a2cd399c50040184a805174a6d89097d9d1559a" + integrity sha512-cdmT3KxjlOQ/gZ2cjfrQOtmhG4HJs6hhvm3mWSRDPtZ/lP5oe8FWceS10JaSJC13GBd4eH/haHnqf7hhGNLerA== + +"@esbuild/linux-arm@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-arm/-/linux-arm-0.19.2.tgz#c32ae97bc0246664a1cfbdb4a98e7b006d7db8ae" + integrity sha512-Odalh8hICg7SOD7XCj0YLpYCEc+6mkoq63UnExDCiRA2wXEmGlK5JVrW50vZR9Qz4qkvqnHcpH+OFEggO3PgTg== + +"@esbuild/linux-ia32@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.17.19.tgz#e28c25266b036ce1cabca3c30155222841dc035a" + integrity sha512-w4IRhSy1VbsNxHRQpeGCHEmibqdTUx61Vc38APcsRbuVgK0OPEnQ0YD39Brymn96mOx48Y2laBQGqgZ0j9w6SQ== + +"@esbuild/linux-ia32@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ia32/-/linux-ia32-0.19.2.tgz#3fc4f0fa026057fe885e4a180b3956e704f1ceaa" + integrity sha512-mLfp0ziRPOLSTek0Gd9T5B8AtzKAkoZE70fneiiyPlSnUKKI4lp+mGEnQXcQEHLJAcIYDPSyBvsUbKUG2ri/XQ== + +"@esbuild/linux-loong64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.17.19.tgz#0f887b8bb3f90658d1a0117283e55dbd4c9dcf72" + integrity sha512-2iAngUbBPMq439a+z//gE+9WBldoMp1s5GWsUSgqHLzLJ9WoZLZhpwWuym0u0u/4XmZ3gpHmzV84PonE+9IIdQ== + +"@esbuild/linux-loong64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-loong64/-/linux-loong64-0.19.2.tgz#633bcaea443f3505fb0ed109ab840c99ad3451a4" + integrity sha512-hn28+JNDTxxCpnYjdDYVMNTR3SKavyLlCHHkufHV91fkewpIyQchS1d8wSbmXhs1fiYDpNww8KTFlJ1dHsxeSw== + +"@esbuild/linux-mips64el@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.17.19.tgz#f5d2a0b8047ea9a5d9f592a178ea054053a70289" + integrity sha512-LKJltc4LVdMKHsrFe4MGNPp0hqDFA1Wpt3jE1gEyM3nKUvOiO//9PheZZHfYRfYl6AwdTH4aTcXSqBerX0ml4A== + +"@esbuild/linux-mips64el@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-mips64el/-/linux-mips64el-0.19.2.tgz#e0bff2898c46f52be7d4dbbcca8b887890805823" + integrity sha512-KbXaC0Sejt7vD2fEgPoIKb6nxkfYW9OmFUK9XQE4//PvGIxNIfPk1NmlHmMg6f25x57rpmEFrn1OotASYIAaTg== + +"@esbuild/linux-ppc64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.17.19.tgz#876590e3acbd9fa7f57a2c7d86f83717dbbac8c7" + integrity sha512-/c/DGybs95WXNS8y3Ti/ytqETiW7EU44MEKuCAcpPto3YjQbyK3IQVKfF6nbghD7EcLUGl0NbiL5Rt5DMhn5tg== + +"@esbuild/linux-ppc64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-ppc64/-/linux-ppc64-0.19.2.tgz#d75798da391f54a9674f8c143b9a52d1dbfbfdde" + integrity sha512-dJ0kE8KTqbiHtA3Fc/zn7lCd7pqVr4JcT0JqOnbj4LLzYnp+7h8Qi4yjfq42ZlHfhOCM42rBh0EwHYLL6LEzcw== + +"@esbuild/linux-riscv64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.17.19.tgz#7f49373df463cd9f41dc34f9b2262d771688bf09" + integrity sha512-FC3nUAWhvFoutlhAkgHf8f5HwFWUL6bYdvLc/TTuxKlvLi3+pPzdZiFKSWz/PF30TB1K19SuCxDTI5KcqASJqA== + +"@esbuild/linux-riscv64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-riscv64/-/linux-riscv64-0.19.2.tgz#012409bd489ed1bb9b775541d4a46c5ded8e6dd8" + integrity sha512-7Z/jKNFufZ/bbu4INqqCN6DDlrmOTmdw6D0gH+6Y7auok2r02Ur661qPuXidPOJ+FSgbEeQnnAGgsVynfLuOEw== + +"@esbuild/linux-s390x@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.17.19.tgz#e2afd1afcaf63afe2c7d9ceacd28ec57c77f8829" + integrity sha512-IbFsFbxMWLuKEbH+7sTkKzL6NJmG2vRyy6K7JJo55w+8xDk7RElYn6xvXtDW8HCfoKBFK69f3pgBJSUSQPr+4Q== + +"@esbuild/linux-s390x@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-s390x/-/linux-s390x-0.19.2.tgz#ece3ed75c5a150de8a5c110f02e97d315761626b" + integrity sha512-U+RinR6aXXABFCcAY4gSlv4CL1oOVvSSCdseQmGO66H+XyuQGZIUdhG56SZaDJQcLmrSfRmx5XZOWyCJPRqS7g== + +"@esbuild/linux-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.17.19.tgz#8a0e9738b1635f0c53389e515ae83826dec22aa4" + integrity sha512-68ngA9lg2H6zkZcyp22tsVt38mlhWde8l3eJLWkyLrp4HwMUr3c1s/M2t7+kHIhvMjglIBrFpncX1SzMckomGw== + +"@esbuild/linux-x64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/linux-x64/-/linux-x64-0.19.2.tgz#dea187019741602d57aaf189a80abba261fbd2aa" + integrity sha512-oxzHTEv6VPm3XXNaHPyUTTte+3wGv7qVQtqaZCrgstI16gCuhNOtBXLEBkBREP57YTd68P0VgDgG73jSD8bwXQ== + +"@esbuild/netbsd-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.17.19.tgz#c29fb2453c6b7ddef9a35e2c18b37bda1ae5c462" + integrity sha512-CwFq42rXCR8TYIjIfpXCbRX0rp1jo6cPIUPSaWwzbVI4aOfX96OXY8M6KNmtPcg7QjYeDmN+DD0Wp3LaBOLf4Q== + +"@esbuild/netbsd-x64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/netbsd-x64/-/netbsd-x64-0.19.2.tgz#bbfd7cf9ab236a23ee3a41b26f0628c57623d92a" + integrity sha512-WNa5zZk1XpTTwMDompZmvQLHszDDDN7lYjEHCUmAGB83Bgs20EMs7ICD+oKeT6xt4phV4NDdSi/8OfjPbSbZfQ== + +"@esbuild/openbsd-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.17.19.tgz#95e75a391403cb10297280d524d66ce04c920691" + integrity sha512-cnq5brJYrSZ2CF6c35eCmviIN3k3RczmHz8eYaVlNasVqsNY+JKohZU5MKmaOI+KkllCdzOKKdPs762VCPC20g== + +"@esbuild/openbsd-x64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/openbsd-x64/-/openbsd-x64-0.19.2.tgz#fa5c4c6ee52a360618f00053652e2902e1d7b4a7" + integrity sha512-S6kI1aT3S++Dedb7vxIuUOb3oAxqxk2Rh5rOXOTYnzN8JzW1VzBd+IqPiSpgitu45042SYD3HCoEyhLKQcDFDw== + +"@esbuild/sunos-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.17.19.tgz#722eaf057b83c2575937d3ffe5aeb16540da7273" + integrity sha512-vCRT7yP3zX+bKWFeP/zdS6SqdWB8OIpaRq/mbXQxTGHnIxspRtigpkUcDMlSCOejlHowLqII7K2JKevwyRP2rg== + +"@esbuild/sunos-x64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/sunos-x64/-/sunos-x64-0.19.2.tgz#52a2ac8ac6284c02d25df22bb4cfde26fbddd68d" + integrity sha512-VXSSMsmb+Z8LbsQGcBMiM+fYObDNRm8p7tkUDMPG/g4fhFX5DEFmjxIEa3N8Zr96SjsJ1woAhF0DUnS3MF3ARw== + +"@esbuild/win32-arm64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.17.19.tgz#9aa9dc074399288bdcdd283443e9aeb6b9552b6f" + integrity sha512-yYx+8jwowUstVdorcMdNlzklLYhPxjniHWFKgRqH7IFlUEa0Umu3KuYplf1HUZZ422e3NU9F4LGb+4O0Kdcaag== + +"@esbuild/win32-arm64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-arm64/-/win32-arm64-0.19.2.tgz#719ed5870855de8537aef8149694a97d03486804" + integrity sha512-5NayUlSAyb5PQYFAU9x3bHdsqB88RC3aM9lKDAz4X1mo/EchMIT1Q+pSeBXNgkfNmRecLXA0O8xP+x8V+g/LKg== + +"@esbuild/win32-ia32@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.17.19.tgz#95ad43c62ad62485e210f6299c7b2571e48d2b03" + integrity sha512-eggDKanJszUtCdlVs0RB+h35wNlb5v4TWEkq4vZcmVt5u/HiDZrTXe2bWFQUez3RgNHwx/x4sk5++4NSSicKkw== + +"@esbuild/win32-ia32@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-ia32/-/win32-ia32-0.19.2.tgz#24832223880b0f581962c8660f8fb8797a1e046a" + integrity sha512-47gL/ek1v36iN0wL9L4Q2MFdujR0poLZMJwhO2/N3gA89jgHp4MR8DKCmwYtGNksbfJb9JoTtbkoe6sDhg2QTA== + +"@esbuild/win32-x64@0.17.19": + version "0.17.19" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.17.19.tgz#8cfaf2ff603e9aabb910e9c0558c26cf32744061" + integrity sha512-lAhycmKnVOuRYNtRtatQR1LPQf2oYCkRGkSFnseDAKPl8lu5SOsK/e1sXe5a0Pc5kHIHe6P2I/ilntNv2xf3cA== + +"@esbuild/win32-x64@0.19.2": + version "0.19.2" + resolved "https://registry.yarnpkg.com/@esbuild/win32-x64/-/win32-x64-0.19.2.tgz#1205014625790c7ff0e471644a878a65d1e34ab0" + integrity sha512-tcuhV7ncXBqbt/Ybf0IyrMcwVOAPDckMK9rXNHtF17UTK18OKLpg08glminN06pt2WCoALhXdLfSPbVvK/6fxw== + +"@supabase/functions-js@^2.1.0": + version "2.1.2" + resolved "https://registry.yarnpkg.com/@supabase/functions-js/-/functions-js-2.1.2.tgz#340a8d3845ef2014338b13a6d33cfa90eb745b14" + integrity sha512-QCR6pwJs9exCl37bmpMisUd6mf+0SUBJ6mUpiAjEkSJ/+xW8TCuO14bvkWHADd5hElJK9MxNlMQXxSA4DRz9nQ== + dependencies: + cross-fetch "^3.1.5" + +"@supabase/gotrue-js@^2.46.1": + version "2.47.0" + resolved "https://registry.yarnpkg.com/@supabase/gotrue-js/-/gotrue-js-2.47.0.tgz#67cca8f7be726fcfcc6dd49f515bbb20b2278f74" + integrity sha512-3e34/vsKH/DoSZCpB85UZpFWSJ2p4GRUUlqgAgeTPagPlx4xS+Nc5v7g7ic7vp3gK0J5PsYVCn9Qu2JQUp4vXg== + dependencies: + cross-fetch "^3.1.5" + +"@supabase/postgrest-js@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@supabase/postgrest-js/-/postgrest-js-1.8.0.tgz#c10b65e120c6523fc947225bcb9131869cf509f1" + integrity sha512-R6leDIC92NgjyG2/tCRJ42rWN7+fZY6ulTEE+c00tcnghn6cX4IYUlnTNMtrdfYC2JYNOTyM+rWj63Wdhr7Zig== + dependencies: + cross-fetch "^3.1.5" + +"@supabase/realtime-js@^2.7.3": + version "2.7.3" + resolved "https://registry.yarnpkg.com/@supabase/realtime-js/-/realtime-js-2.7.3.tgz#cbcb84181add681ab99c87032bfe88101c6863b3" + integrity sha512-c7TzL81sx2kqyxsxcDduJcHL9KJdCOoKimGP6lQSqiZKX42ATlBZpWbyy9KFGFBjAP4nyopMf5JhPi2ZH9jyNw== + dependencies: + "@types/phoenix" "^1.5.4" + "@types/websocket" "^1.0.3" + websocket "^1.0.34" + +"@supabase/storage-js@^2.5.1": + version "2.5.1" + resolved "https://registry.yarnpkg.com/@supabase/storage-js/-/storage-js-2.5.1.tgz#16c4c088996e0395034717836e626f14df63a349" + integrity sha512-nkR0fQA9ScAtIKA3vNoPEqbZv1k5B5HVRYEvRWdlP6mUpFphM9TwPL2jZ/ztNGMTG5xT6SrHr+H7Ykz8qzbhjw== + dependencies: + cross-fetch "^3.1.5" + +"@supabase/supabase-js@^2.32.0": + version "2.32.0" + resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.32.0.tgz#863c636d83232c6a2e9ba5932e0d7c1bf80bc436" + integrity sha512-1ShFhuOI5Du7604nlCelBsRD61daXk2O0qwjumoz35bqrYThnSPPtpJqZOHw6Mg6o7mLjIInYLh/DBlh8UvzRg== + dependencies: + "@supabase/functions-js" "^2.1.0" + "@supabase/gotrue-js" "^2.46.1" + "@supabase/postgrest-js" "^1.8.0" + "@supabase/realtime-js" "^2.7.3" + "@supabase/storage-js" "^2.5.1" + cross-fetch "^3.1.5" + +"@types/node@*": + version "20.5.1" + resolved "https://registry.yarnpkg.com/@types/node/-/node-20.5.1.tgz#178d58ee7e4834152b0e8b4d30cbfab578b9bb30" + integrity sha512-4tT2UrL5LBqDwoed9wZ6N3umC4Yhz3W3FloMmiiG4JwmUJWpie0c7lcnUNd4gtMKuDEO4wRVS8B6Xa0uMRsMKg== + +"@types/phoenix@^1.5.4": + version "1.6.0" + resolved "https://registry.yarnpkg.com/@types/phoenix/-/phoenix-1.6.0.tgz#eb7536259ee695646e75c4c7b0c9a857ea174781" + integrity sha512-qwfpsHmFuhAS/dVd4uBIraMxRd56vwBUYQGZ6GpXnFuM2XMRFJbIyruFKKlW2daQliuYZwe0qfn/UjFCDKic5g== + +"@types/websocket@^1.0.3": + version "1.0.5" + resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.5.tgz#3fb80ed8e07f88e51961211cd3682a3a4a81569c" + integrity sha512-NbsqiNX9CnEfC1Z0Vf4mE1SgAJ07JnRYcNex7AJ9zAVzmiGHmjKFEk7O4TJIsgv2B1sLEb6owKFZrACwdYngsQ== + dependencies: + "@types/node" "*" + +buffer-from@^1.0.0: + version "1.1.2" + resolved "https://registry.yarnpkg.com/buffer-from/-/buffer-from-1.1.2.tgz#2b146a6fd72e80b4f55d255f35ed59a3a9a41bd5" + integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== + +bufferutil@^4.0.1: + version "4.0.7" + resolved "https://registry.yarnpkg.com/bufferutil/-/bufferutil-4.0.7.tgz#60c0d19ba2c992dd8273d3f73772ffc894c153ad" + integrity sha512-kukuqc39WOHtdxtw4UScxF/WVnMFVSQVKhtx3AjZJzhd0RGZZldcrfSEbVsWWe6KNH253574cq5F+wpv0G9pJw== + dependencies: + node-gyp-build "^4.3.0" + +cross-fetch@^3.1.5: + version "3.1.8" + resolved "https://registry.yarnpkg.com/cross-fetch/-/cross-fetch-3.1.8.tgz#0327eba65fd68a7d119f8fb2bf9334a1a7956f82" + integrity sha512-cvA+JwZoU0Xq+h6WkMvAUqPEYy92Obet6UdKLfW60qn99ftItKjB5T+BkyWOFWe2pUyfQ+IJHmpOTznqk1M6Kg== + dependencies: + node-fetch "^2.6.12" + +d@1, d@^1.0.1: + version "1.0.1" + resolved "https://registry.yarnpkg.com/d/-/d-1.0.1.tgz#8698095372d58dbee346ffd0c7093f99f8f9eb5a" + integrity sha512-m62ShEObQ39CfralilEQRjH6oAMtNCV1xJyEx5LpRYUVN+EviphDgUc/F3hnYbADmkiNs67Y+3ylmlG7Lnu+FA== + dependencies: + es5-ext "^0.10.50" + type "^1.0.1" + +debug@^2.2.0: + version "2.6.9" + resolved "https://registry.yarnpkg.com/debug/-/debug-2.6.9.tgz#5d128515df134ff327e90a4c93f4e077a536341f" + integrity sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA== + dependencies: + ms "2.0.0" + +es5-ext@^0.10.35, es5-ext@^0.10.50: + version "0.10.62" + resolved "https://registry.yarnpkg.com/es5-ext/-/es5-ext-0.10.62.tgz#5e6adc19a6da524bf3d1e02bbc8960e5eb49a9a5" + integrity sha512-BHLqn0klhEpnOKSrzn/Xsz2UIW8j+cGmo9JLzr8BiUapV8hPL9+FliFqjwr9ngW7jWdnxv6eO+/LqyhJVqgrjA== + dependencies: + es6-iterator "^2.0.3" + es6-symbol "^3.1.3" + next-tick "^1.1.0" + +es6-iterator@^2.0.3: + version "2.0.3" + resolved "https://registry.yarnpkg.com/es6-iterator/-/es6-iterator-2.0.3.tgz#a7de889141a05a94b0854403b2d0a0fbfa98f3b7" + integrity sha512-zw4SRzoUkd+cl+ZoE15A9o1oQd920Bb0iOJMQkQhl3jNc03YqVjAhG7scf9C5KWRU/R13Orf588uCC6525o02g== + dependencies: + d "1" + es5-ext "^0.10.35" + es6-symbol "^3.1.1" + +es6-symbol@^3.1.1, es6-symbol@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/es6-symbol/-/es6-symbol-3.1.3.tgz#bad5d3c1bcdac28269f4cb331e431c78ac705d18" + integrity sha512-NJ6Yn3FuDinBaBRWl/q5X/s4koRHBrgKAu+yGI6JCBeiu3qrcbJhwT2GeR/EXVfylRk8dpQVJoLEFhK+Mu31NA== + dependencies: + d "^1.0.1" + ext "^1.1.2" + +esbuild@^0.19.2: + version "0.19.2" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.19.2.tgz#b1541828a89dfb6f840d38538767c6130dca2aac" + integrity sha512-G6hPax8UbFakEj3hWO0Vs52LQ8k3lnBhxZWomUJDxfz3rZTLqF5k/FCzuNdLx2RbpBiQQF9H9onlDDH1lZsnjg== + optionalDependencies: + "@esbuild/android-arm" "0.19.2" + "@esbuild/android-arm64" "0.19.2" + "@esbuild/android-x64" "0.19.2" + "@esbuild/darwin-arm64" "0.19.2" + "@esbuild/darwin-x64" "0.19.2" + "@esbuild/freebsd-arm64" "0.19.2" + "@esbuild/freebsd-x64" "0.19.2" + "@esbuild/linux-arm" "0.19.2" + "@esbuild/linux-arm64" "0.19.2" + "@esbuild/linux-ia32" "0.19.2" + "@esbuild/linux-loong64" "0.19.2" + "@esbuild/linux-mips64el" "0.19.2" + "@esbuild/linux-ppc64" "0.19.2" + "@esbuild/linux-riscv64" "0.19.2" + "@esbuild/linux-s390x" "0.19.2" + "@esbuild/linux-x64" "0.19.2" + "@esbuild/netbsd-x64" "0.19.2" + "@esbuild/openbsd-x64" "0.19.2" + "@esbuild/sunos-x64" "0.19.2" + "@esbuild/win32-arm64" "0.19.2" + "@esbuild/win32-ia32" "0.19.2" + "@esbuild/win32-x64" "0.19.2" + +esbuild@~0.17.6: + version "0.17.19" + resolved "https://registry.yarnpkg.com/esbuild/-/esbuild-0.17.19.tgz#087a727e98299f0462a3d0bcdd9cd7ff100bd955" + integrity sha512-XQ0jAPFkK/u3LcVRcvVHQcTIqD6E2H1fvZMA5dQPSOWb3suUbWbfbRf94pjc0bNzRYLfIrDRQXr7X+LHIm5oHw== + optionalDependencies: + "@esbuild/android-arm" "0.17.19" + "@esbuild/android-arm64" "0.17.19" + "@esbuild/android-x64" "0.17.19" + "@esbuild/darwin-arm64" "0.17.19" + "@esbuild/darwin-x64" "0.17.19" + "@esbuild/freebsd-arm64" "0.17.19" + "@esbuild/freebsd-x64" "0.17.19" + "@esbuild/linux-arm" "0.17.19" + "@esbuild/linux-arm64" "0.17.19" + "@esbuild/linux-ia32" "0.17.19" + "@esbuild/linux-loong64" "0.17.19" + "@esbuild/linux-mips64el" "0.17.19" + "@esbuild/linux-ppc64" "0.17.19" + "@esbuild/linux-riscv64" "0.17.19" + "@esbuild/linux-s390x" "0.17.19" + "@esbuild/linux-x64" "0.17.19" + "@esbuild/netbsd-x64" "0.17.19" + "@esbuild/openbsd-x64" "0.17.19" + "@esbuild/sunos-x64" "0.17.19" + "@esbuild/win32-arm64" "0.17.19" + "@esbuild/win32-ia32" "0.17.19" + "@esbuild/win32-x64" "0.17.19" + +ext@^1.1.2: + version "1.7.0" + resolved "https://registry.yarnpkg.com/ext/-/ext-1.7.0.tgz#0ea4383c0103d60e70be99e9a7f11027a33c4f5f" + integrity sha512-6hxeJYaL110a9b5TEJSj0gojyHQAmA2ch5Os+ySCiA1QGdS697XWY1pzsrSjqA9LDEEgdB/KypIlR59RcLuHYw== + dependencies: + type "^2.7.2" + +fsevents@~2.3.2: + version "2.3.3" + resolved "https://registry.yarnpkg.com/fsevents/-/fsevents-2.3.3.tgz#cac6407785d03675a2a5e1a5305c697b347d90d6" + integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== + +get-tsconfig@^4.4.0: + version "4.7.0" + resolved "https://registry.yarnpkg.com/get-tsconfig/-/get-tsconfig-4.7.0.tgz#06ce112a1463e93196aa90320c35df5039147e34" + integrity sha512-pmjiZ7xtB8URYm74PlGJozDNyhvsVLUcpBa8DZBG3bWHwaHa9bPiRpiSfovw+fjhwONSCWKRyk+JQHEGZmMrzw== + dependencies: + resolve-pkg-maps "^1.0.0" + +is-typedarray@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/is-typedarray/-/is-typedarray-1.0.0.tgz#e479c80858df0c1b11ddda6940f96011fcda4a9a" + integrity sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA== + +ms@2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" + integrity sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A== + +next-tick@^1.1.0: + version "1.1.0" + resolved "https://registry.yarnpkg.com/next-tick/-/next-tick-1.1.0.tgz#1836ee30ad56d67ef281b22bd199f709449b35eb" + integrity sha512-CXdUiJembsNjuToQvxayPZF9Vqht7hewsvy2sOWafLvi2awflj9mOC6bHIg50orX8IJvWKY9wYQ/zB2kogPslQ== + +node-fetch@^2.6.12: + version "2.6.13" + resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.13.tgz#a20acbbec73c2e09f9007de5cda17104122e0010" + integrity sha512-StxNAxh15zr77QvvkmveSQ8uCQ4+v5FkvNTj0OESmiHu+VRi/gXArXtkWMElOsOUNLtUEvI4yS+rdtOHZTwlQA== + dependencies: + whatwg-url "^5.0.0" + +node-gyp-build@^4.3.0: + version "4.6.0" + resolved "https://registry.yarnpkg.com/node-gyp-build/-/node-gyp-build-4.6.0.tgz#0c52e4cbf54bbd28b709820ef7b6a3c2d6209055" + integrity sha512-NTZVKn9IylLwUzaKjkas1e4u2DLNcV4rdYagA4PWdPwW87Bi7z+BznyKSRwS/761tV/lzCGXplWsiaMjLqP2zQ== + +resolve-pkg-maps@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz#616b3dc2c57056b5588c31cdf4b3d64db133720f" + integrity sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw== + +source-map-support@^0.5.21: + version "0.5.21" + resolved "https://registry.yarnpkg.com/source-map-support/-/source-map-support-0.5.21.tgz#04fe7c7f9e1ed2d662233c28cb2b35b9f63f6e4f" + integrity sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w== + dependencies: + buffer-from "^1.0.0" + source-map "^0.6.0" + +source-map@^0.6.0: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + integrity sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g== + +tr46@~0.0.3: + version "0.0.3" + resolved "https://registry.yarnpkg.com/tr46/-/tr46-0.0.3.tgz#8184fd347dac9cdc185992f3a6622e14b9d9ab6a" + integrity sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw== + +tsx@^3.12.7: + version "3.12.7" + resolved "https://registry.yarnpkg.com/tsx/-/tsx-3.12.7.tgz#b3b8b0fc79afc8260d1e14f9e995616c859a91e9" + integrity sha512-C2Ip+jPmqKd1GWVQDvz/Eyc6QJbGfE7NrR3fx5BpEHMZsEHoIxHL1j+lKdGobr8ovEyqeNkPLSKp6SCSOt7gmw== + dependencies: + "@esbuild-kit/cjs-loader" "^2.4.2" + "@esbuild-kit/core-utils" "^3.0.0" + "@esbuild-kit/esm-loader" "^2.5.5" + optionalDependencies: + fsevents "~2.3.2" + +type@^1.0.1: + version "1.2.0" + resolved "https://registry.yarnpkg.com/type/-/type-1.2.0.tgz#848dd7698dafa3e54a6c479e759c4bc3f18847a0" + integrity sha512-+5nt5AAniqsCnu2cEQQdpzCAh33kVx8n0VoFidKpB1dVVLAN/F+bgVOqOJqOnEnrhp222clB5p3vUlD+1QAnfg== + +type@^2.7.2: + version "2.7.2" + resolved "https://registry.yarnpkg.com/type/-/type-2.7.2.tgz#2376a15a3a28b1efa0f5350dcf72d24df6ef98d0" + integrity sha512-dzlvlNlt6AXU7EBSfpAscydQ7gXB+pPGsPnfJnZpiNJBDj7IaJzQlBZYGdEi4R9HmPdBv2XmWJ6YUtoTa7lmCw== + +typedarray-to-buffer@^3.1.5: + version "3.1.5" + resolved "https://registry.yarnpkg.com/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz#a97ee7a9ff42691b9f783ff1bc5112fe3fca9080" + integrity sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q== + dependencies: + is-typedarray "^1.0.0" + +typescript@^4.3.2: + version "4.9.5" + resolved "https://registry.yarnpkg.com/typescript/-/typescript-4.9.5.tgz#095979f9bcc0d09da324d58d03ce8f8374cbe65a" + integrity sha512-1FXk9E2Hm+QzZQ7z+McJiHL4NW1F2EzMu9Nq9i3zAaGqibafqYwCVU6WyWAuyQRRzOlxou8xZSyXLEN8oKj24g== + +utf-8-validate@^5.0.2: + version "5.0.10" + resolved "https://registry.yarnpkg.com/utf-8-validate/-/utf-8-validate-5.0.10.tgz#d7d10ea39318171ca982718b6b96a8d2442571a2" + integrity sha512-Z6czzLq4u8fPOyx7TU6X3dvUZVvoJmxSQ+IcrlmagKhilxlhZgxPK6C5Jqbkw1IDUmFTM+cz9QDnnLTwDz/2gQ== + dependencies: + node-gyp-build "^4.3.0" + +webidl-conversions@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/webidl-conversions/-/webidl-conversions-3.0.1.tgz#24534275e2a7bc6be7bc86611cc16ae0a5654871" + integrity sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ== + +websocket@^1.0.34: + version "1.0.34" + resolved "https://registry.yarnpkg.com/websocket/-/websocket-1.0.34.tgz#2bdc2602c08bf2c82253b730655c0ef7dcab3111" + integrity sha512-PRDso2sGwF6kM75QykIesBijKSVceR6jL2G8NGYyq2XrItNC2P5/qL5XeR056GhA+Ly7JMFvJb9I312mJfmqnQ== + dependencies: + bufferutil "^4.0.1" + debug "^2.2.0" + es5-ext "^0.10.50" + typedarray-to-buffer "^3.1.5" + utf-8-validate "^5.0.2" + yaeti "^0.0.6" + +whatwg-url@^5.0.0: + version "5.0.0" + resolved "https://registry.yarnpkg.com/whatwg-url/-/whatwg-url-5.0.0.tgz#966454e8765462e37644d3626f6742ce8b70965d" + integrity sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw== + dependencies: + tr46 "~0.0.3" + webidl-conversions "^3.0.0" + +yaeti@^0.0.6: + version "0.0.6" + resolved "https://registry.yarnpkg.com/yaeti/-/yaeti-0.0.6.tgz#f26f484d72684cf42bedfb76970aa1608fbf9577" + integrity sha512-MvQa//+KcZCUkBTIC9blM+CU9J2GzuTytsOUwf2lidtvkx/6gnEp1QvJv34t9vdjhFmha/mUiNDbN0D0mJWdug== diff --git a/package.json b/package.json index 8d05c1033..e64d64e99 100644 --- a/package.json +++ b/package.json @@ -14,27 +14,25 @@ "build:ci": "ncc build src/adapters/github/github-actions.ts -o ./", "build:serverless": "ncc build src/index.ts -o ./", "build": "tsc", + "postbuild": "copyfiles src/assets/images/* lib/", "clean": "rimraf ./dist ./lib ./node_modules", "format:check": "prettier -c src/**/*.ts", "format": "prettier --write src", "lint": "eslint --ext .ts ./src", - "prestart:watch": "ln -s src/assets/images lib/assets/images", "start:serverless": "tsx src/adapters/github/github-actions.ts", "start:watch": "nodemon --exec 'yarn start'", - "start": "probot run ./lib/index.js", + "start": "probot run ./lib/src/index.js", "prepare": "husky install" }, "dependencies": { "@actions/core": "^1.10.0", "@commitlint/cli": "^17.4.3", "@commitlint/config-conventional": "^17.4.3", - "@logdna/logger": "^2.6.6", "@netlify/functions": "^1.4.0", "@probot/adapter-aws-lambda-serverless": "^3.0.2", "@probot/adapter-github-actions": "^3.1.3", "@sinclair/typebox": "^0.25.9", "@supabase/supabase-js": "^2.4.0", - "@types/mdast": "^3.0.11", "@types/ms": "^0.7.31", "@typescript-eslint/eslint-plugin": "^5.59.11", "@typescript-eslint/parser": "^5.59.11", @@ -43,19 +41,20 @@ "ajv": "^8.11.2", "ajv-formats": "^2.1.1", "axios": "^1.3.2", + "decimal.js": "^10.4.3", + "copyfiles": "^2.4.1", "ethers": "^5.7.2", + "exponential-backoff": "^3.1.1", "husky": "^8.0.2", "jimp": "^0.22.4", "js-yaml": "^4.1.0", "libsodium-wrappers": "^0.7.11", "lint-staged": "^13.1.0", - "mdast-util-from-markdown": "^1.3.0", - "mdast-util-gfm": "^2.0.2", - "micromark-extension-gfm": "^2.0.3", "ms": "^2.1.3", "node-html-parser": "^6.1.5", "node-html-to-image": "^3.3.0", "nodemon": "^2.0.19", + "parse5": "^7.1.2", "prettier": "^2.7.1", "probot": "^12.2.4", "telegraf": "^4.11.2", diff --git a/src/adapters/supabase/helpers/client.ts b/src/adapters/supabase/helpers/client.ts index f92d2e4e3..cbd0629f7 100644 --- a/src/adapters/supabase/helpers/client.ts +++ b/src/adapters/supabase/helpers/client.ts @@ -2,6 +2,7 @@ import { createClient, SupabaseClient } from "@supabase/supabase-js"; import { getAdapters, getLogger } from "../../../bindings"; import { Issue, UserProfile } from "../../../types"; import { Database } from "../types"; +import { InsertPermit, Permit } from "../../../helpers"; import { BigNumber, BigNumberish } from "ethers"; /** @@ -12,7 +13,7 @@ import { BigNumber, BigNumberish } from "ethers"; * @returns - The supabase client */ export const supabase = (url: string, key: string): SupabaseClient => { - return createClient(url, key); + return createClient(url, key, { auth: { persistSession: false } }); }; /** @@ -32,25 +33,45 @@ export const getMaxIssueNumber = async (): Promise => { /** * @dev Gets the last weekly update timestamp */ -export const getLastWeeklyTime = async (): Promise => { +export const getLastWeeklyTime = async (): Promise => { const { supabase } = getAdapters(); const { data } = await supabase.from("weekly").select("last_time").limit(1).single(); if (data) { - return Number(data.last_time); + return new Date(data.last_time); } else { - return 0; + return undefined; } }; /** * @dev Updates the last weekly update timestamp */ -export const updateLastWeeklyTime = async (time: number): Promise => { +export const updateLastWeeklyTime = async (time: Date): Promise => { const logger = getLogger(); const { supabase } = getAdapters(); - const { data, error } = await supabase.from("weekly").update({ last_time: time }); - logger.info(`Updating last time is done, data: ${data}, error: ${error}`); + + const { data, error } = await supabase.from("weekly").select("last_time"); + if (error) { + logger.error(`Checking last time failed, error: ${JSON.stringify(error)}`); + throw new Error(`Checking last time failed, error: ${JSON.stringify(error)}`); + } + + if (data && data.length > 0) { + const { data, error } = await supabase.from("weekly").update({ last_time: time.toUTCString() }).neq("last_time", time.toUTCString()); + if (error) { + logger.error(`Updating last time failed, error: ${JSON.stringify(error)}`); + throw new Error(`Updating last time failed, error: ${JSON.stringify(error)}`); + } + logger.info(`Updating last time is done, data: ${data}`); + } else { + const { data, error } = await supabase.from("weekly").insert({ last_time: time.toUTCString() }); + if (error) { + logger.error(`Creating last time failed, error: ${JSON.stringify(error)}`); + throw new Error(`Creating last time failed, error: ${JSON.stringify(error)}`); + } + logger.info(`Creating last time is done, data: ${data}`); + } return; }; @@ -91,7 +112,7 @@ const getDbDataFromUserProfile = (userProfile: UserProfile, additions?: UserProf return { user_login: userProfile.login, user_type: userProfile.type, - user_name: userProfile.name, + user_name: userProfile.name ?? userProfile.login, company: userProfile.company, blog: userProfile.blog, user_location: userProfile.location, @@ -113,18 +134,30 @@ const getDbDataFromUserProfile = (userProfile: UserProfile, additions?: UserProf export const upsertIssue = async (issue: Issue, additions: IssueAdditions): Promise => { const logger = getLogger(); const { supabase } = getAdapters(); - const { data, error } = await supabase.from("issues").select("id").eq("issue_number", issue.number).single(); + const { data, error } = await supabase.from("issues").select("id").eq("issue_number", issue.number); + if (error) { + logger.error(`Checking issue failed, error: ${JSON.stringify(error)}`); + throw new Error(`Checking issue failed, error: ${JSON.stringify(error)}`); + } - if (data) { - const key = data.id as number; - await supabase + if (data && data.length > 0) { + const key = data[0].id as number; + const { data: _data, error: _error } = await supabase .from("issues") .upsert({ id: key, ...getDbDataFromIssue(issue, additions) }) .select(); - logger.info(`Upserting an issue done, data: ${data}, error: ${error}`); - } else if (error) { + if (_error) { + logger.error(`Upserting an issue failed, error: ${JSON.stringify(_error)}`); + throw new Error(`Upserting an issue failed, error: ${JSON.stringify(_error)}`); + } + logger.info(`Upserting an issue done, { data: ${_data}, error: ${_error}`); + } else { const { data: _data, error: _error } = await supabase.from("issues").insert(getDbDataFromIssue(issue, additions)); - logger.info(`Creating a new issue done, { data: ${_data}, error: ${_error}`); + if (_error) { + logger.error(`Creating a new issue record failed, error: ${JSON.stringify(_error)}`); + throw new Error(`Creating a new issue record failed, error: ${JSON.stringify(_error)}`); + } + logger.info(`Creating a new issue record done, { data: ${_data}, error: ${_error}`); } }; @@ -135,18 +168,26 @@ export const upsertIssue = async (issue: Issue, additions: IssueAdditions): Prom export const upsertUser = async (user: UserProfile): Promise => { const logger = getLogger(); const { supabase } = getAdapters(); - const { data, error } = await supabase.from("users").select("id").eq("user_login", user.login).single(); + const { data, error } = await supabase.from("users").select("user_login").eq("user_login", user.login); + if (error) { + logger.error(`Checking user failed, error: ${JSON.stringify(error)}`); + throw new Error(`Checking user failed, error: ${JSON.stringify(error)}`); + } - if (data) { - const key = data.id as number; - await supabase - .from("users") - .upsert({ id: key, ...getDbDataFromUserProfile(user) }) - .select(); - logger.info(`Upserting an user done", { data: ${data}, error: ${error} }`); - } else if (error) { + if (data && data.length > 0) { + const { data: _data, error: _error } = await supabase.from("users").upsert(getDbDataFromUserProfile(user)).select(); + if (_error) { + logger.error(`Upserting a user failed, error: ${JSON.stringify(_error)}`); + throw new Error(`Upserting a user failed, error: ${JSON.stringify(_error)}`); + } + logger.info(`Upserting a user done, { data: ${JSON.stringify(_data)} }`); + } else { const { data: _data, error: _error } = await supabase.from("users").insert(getDbDataFromUserProfile(user)); - logger.info(`Creating a new user done", { data: ${_data}, error: ${_error} }`); + if (_error) { + logger.error(`Creating a new user record failed, error: ${JSON.stringify(_error)}`); + throw new Error(`Creating a new user record failed, error: ${JSON.stringify(_error)}`); + } + logger.info(`Creating a new user record done, { data: ${JSON.stringify(_data)} }`); } }; @@ -159,14 +200,23 @@ export const upsertWalletAddress = async (username: string, address: string): Pr const logger = getLogger(); const { supabase } = getAdapters(); - const { data, error } = await supabase.from("wallets").select("user_name").eq("user_name", username).single(); - if (data) { - await supabase.from("wallets").upsert({ + const { data, error } = await supabase.from("wallets").select("user_name").eq("user_name", username); + if (error) { + logger.error(`Checking wallet address failed, error: ${JSON.stringify(error)}`); + throw new Error(`Checking wallet address failed, error: ${JSON.stringify(error)}`); + } + + if (data && data.length > 0) { + const { data: _data, error: _error } = await supabase.from("wallets").upsert({ user_name: username, wallet_address: address, updated_at: new Date().toUTCString(), }); - logger.info(`Upserting a wallet address done, { data: ${data}, error: ${error} }`); + if (_error) { + logger.error(`Upserting a wallet address failed, error: ${JSON.stringify(_error)}`); + throw new Error(`Upserting a wallet address failed, error: ${JSON.stringify(_error)}`); + } + logger.info(`Upserting a wallet address done, { data: ${JSON.stringify(_data)} }`); } else { const { data: _data, error: _error } = await supabase.from("wallets").insert({ user_name: username, @@ -174,7 +224,11 @@ export const upsertWalletAddress = async (username: string, address: string): Pr created_at: new Date().toUTCString(), updated_at: new Date().toUTCString(), }); - logger.info(`Creating a new wallet_table record done, { data: ${_data}, error: ${_error} }`); + if (_error) { + logger.error(`Creating a new wallet_table record failed, error: ${JSON.stringify(_error)}`); + throw new Error(`Creating a new wallet_table record failed, error: ${JSON.stringify(_error)}`); + } + logger.info(`Creating a new wallet_table record done, { data: ${JSON.stringify(_data)} }`); } }; @@ -187,15 +241,24 @@ export const upsertWalletMultiplier = async (username: string, multiplier: strin const logger = getLogger(); const { supabase } = getAdapters(); - const { data, error } = await supabase.from("multiplier").select("user_id").eq("user_id", `${username}_${org_id}`).single(); - if (data) { - await supabase.from("multiplier").upsert({ + const { data, error } = await supabase.from("multiplier").select("user_id").eq("user_id", `${username}_${org_id}`); + if (error) { + logger.error(`Checking wallet multiplier failed, error: ${JSON.stringify(error)}`); + throw new Error(`Checking wallet multiplier failed, error: ${JSON.stringify(error)}`); + } + + if (data && data.length > 0) { + const { data: _data, error: _error } = await supabase.from("multiplier").upsert({ user_id: `${username}_${org_id}`, value: multiplier, reason, updated_at: new Date().toUTCString(), }); - logger.info(`Upserting a wallet address done, { data: ${data}, error: ${error} }`); + if (_error) { + logger.error(`Upserting a wallet multiplier failed, error: ${JSON.stringify(_error)}`); + throw new Error(`Upserting a wallet multiplier failed, error: ${JSON.stringify(_error)}`); + } + logger.info(`Upserting a wallet multiplier done, { data: ${JSON.stringify(_data)} }`); } else { const { data: _data, error: _error } = await supabase.from("multiplier").insert({ user_id: `${username}_${org_id}`, @@ -204,7 +267,11 @@ export const upsertWalletMultiplier = async (username: string, multiplier: strin created_at: new Date().toUTCString(), updated_at: new Date().toUTCString(), }); - logger.info(`Creating a new multiplier_table record done, { data: ${_data}, error: ${_error} }`); + if (_error) { + logger.error(`Creating a new multiplier record failed, error: ${JSON.stringify(_error)}`); + throw new Error(`Creating a new multiplier record failed, error: ${JSON.stringify(_error)}`); + } + logger.info(`Creating a new multiplier record done, { data: ${JSON.stringify(_data)} }`); } }; @@ -219,7 +286,11 @@ export const upsertAccessControl = async (username: string, repository: string, const logger = getLogger(); const { supabase } = getAdapters(); - const { data, error } = await supabase.from("access").select("user_name").eq("user_name", username).eq("repository", repository).single(); + const { data, error } = await supabase.from("access").select("user_name").eq("user_name", username).eq("repository", repository); + if (error) { + logger.error(`Checking access control failed, error: ${JSON.stringify(error)}`); + throw new Error(`Checking access control failed, error: ${JSON.stringify(error)}`); + } const properties = { user_name: username, @@ -228,9 +299,13 @@ export const upsertAccessControl = async (username: string, repository: string, [access]: bool, }; - if (data) { - await supabase.from("access").upsert(properties); - logger.info(`Upserting an access done, { data: ${data}, error: ${error} }`); + if (data && data.length > 0) { + const { data: _data, error: _error } = await supabase.from("access").upsert(properties); + if (_error) { + logger.error(`Upserting a access control failed, error: ${JSON.stringify(_error)}`); + throw new Error(`Upserting a access control failed, error: ${JSON.stringify(_error)}`); + } + logger.info(`Upserting a access control done, { data: ${JSON.stringify(_data)} }`); } else { const { data: _data, error: _error } = await supabase.from("access").insert({ created_at: new Date().toUTCString(), @@ -240,7 +315,11 @@ export const upsertAccessControl = async (username: string, repository: string, priority_access: false, ...properties, }); - logger.info(`Creating a new access record done, { data: ${_data}, error: ${_error} }`); + if (_error) { + logger.error(`Creating a new access control record failed, error: ${JSON.stringify(_error)}`); + throw new Error(`Creating a new access control record failed, error: ${JSON.stringify(_error)}`); + } + logger.info(`Creating a new access control record done, { data: ${JSON.stringify(_data)} }`); } }; @@ -366,3 +445,58 @@ export const removePenalty = async (username: string, repoName: string, tokenAdd throw new Error(`Error removing penalty: ${error.message}`); } }; + +const getDbDataFromPermit = (permit: InsertPermit): Record => { + return { + organization_id: permit.organizationId, + repository_id: permit.repositoryId, + issue_id: permit.issueId, + network_id: permit.networkId, + bounty_hunter_id: permit.bountyHunterId, + token_address: permit.tokenAddress, + payout_amount: permit.payoutAmount, + bounty_hunter_address: permit.bountyHunterAddress, + nonce: permit.nonce, + deadline: permit.deadline, + signature: permit.signature, + wallet_owner_address: permit.walletOwnerAddress, + }; +}; + +const getPermitFromDbData = (data: Record): Permit => { + return { + id: data.id, + createdAt: new Date(Date.parse(data.created_at as string)), + organizationId: data.organization_id, + repositoryId: data.repository_i, + issueId: data.issue_id, + networkId: data.network_id, + bountyHunterId: data.bounty_hunter_id, + tokenAddress: data.token_address, + payoutAmount: data.payout_amount, + bountyHunterAddress: data.bounty_hunter_address, + nonce: data.nonce, + deadline: data.deadline, + signature: data.signature, + walletOwnerAddress: data.wallet_owner_address, + } as Permit; +}; + +export const savePermit = async (permit: InsertPermit): Promise => { + const { supabase } = getAdapters(); + const { data, error } = await supabase + .from("permits") + .insert({ + ...getDbDataFromPermit(permit), + created_at: new Date().toISOString(), + id: undefined, // id is auto-generated + }) + .select(); + if (error) { + throw new Error(error.message); + } + if (!data || data.length === 0) { + throw new Error("No data returned"); + } + return getPermitFromDbData(data[0]); +}; diff --git a/src/adapters/supabase/helpers/index.ts b/src/adapters/supabase/helpers/index.ts index 5ec76921e..3bb7e5e98 100644 --- a/src/adapters/supabase/helpers/index.ts +++ b/src/adapters/supabase/helpers/index.ts @@ -1 +1,2 @@ export * from "./client"; +export * from "./log"; diff --git a/src/adapters/supabase/helpers/log.ts b/src/adapters/supabase/helpers/log.ts new file mode 100644 index 000000000..e352339a8 --- /dev/null +++ b/src/adapters/supabase/helpers/log.ts @@ -0,0 +1,193 @@ +import { getAdapters, getBotContext, Logger } from "../../../bindings"; +import { Payload } from "../../../types"; +import { getNumericLevel } from "../../../utils/helpers"; +import { getOrgAndRepoFromPath } from "../../../utils/private"; +interface Log { + repo: string | null; + org: string | null; + commentId: number | undefined; + issueNumber: number | undefined; + logMessage: string; + level: Level; + timestamp: string; +} + +export enum Level { + ERROR = "error", + WARN = "warn", + INFO = "info", + HTTP = "http", + VERBOSE = "verbose", + DEBUG = "debug", + SILLY = "silly", +} + +export class GitHubLogger implements Logger { + private supabase; + private maxLevel; + private app; + private logEnvironment; + private logQueue: Log[] = []; // Your log queue + private maxConcurrency = 6; // Maximum concurrent requests + private retryDelay = 1000; // Delay between retries in milliseconds + private throttleCount = 0; + private retryLimit = 0; // Retries disabled by default + + constructor(app: string, logEnvironment: string, maxLevel: Level, retryLimit: number) { + this.app = app; + this.logEnvironment = logEnvironment; + this.maxLevel = getNumericLevel(maxLevel); + this.retryLimit = retryLimit; + this.supabase = getAdapters().supabase; + } + + async sendLogsToSupabase({ repo, org, commentId, issueNumber, logMessage, level, timestamp }: Log) { + const { error } = await this.supabase.from("logs").insert([ + { + repo_name: repo, + level: getNumericLevel(level), + org_name: org, + comment_id: commentId, + log_message: logMessage, + issue_number: issueNumber, + timestamp, + }, + ]); + + if (error) { + console.error("Error logging to Supabase:", error.message); + return; + } + } + + async processLogs(log: Log) { + try { + await this.sendLogsToSupabase(log); + } catch (error) { + console.error("Error sending log, retrying:", error); + return this.retryLimit > 0 ? await this.retryLog(log) : null; + } + } + + async retryLog(log: Log, retryCount = 0) { + if (retryCount >= this.retryLimit) { + console.error("Max retry limit reached for log:", log); + return; + } + + await new Promise((resolve) => setTimeout(resolve, this.retryDelay)); + + try { + await this.sendLogsToSupabase(log); + } catch (error) { + console.error("Error sending log (after retry):", error); + await this.retryLog(log, retryCount + 1); + } + } + + async processLogQueue() { + while (this.logQueue.length > 0) { + const log = this.logQueue.shift(); + if (!log) { + continue; + } + await this.processLogs(log); + } + } + + async throttle() { + if (this.throttleCount >= this.maxConcurrency) { + return; + } + + this.throttleCount++; + try { + await this.processLogQueue(); + } finally { + this.throttleCount--; + if (this.logQueue.length > 0) { + await this.throttle(); + } + } + } + + async addToQueue(log: Log) { + this.logQueue.push(log); + if (this.throttleCount < this.maxConcurrency) { + await this.throttle(); + } + } + + private save(logMessage: string | object, level: Level, errorPayload?: string | object) { + if (getNumericLevel(level) > this.maxLevel) return; // only return errors lower than max level + + const context = getBotContext(); + const payload = context.payload as Payload; + const timestamp = new Date().toUTCString(); + + const { comment, issue, repository } = payload; + const commentId = comment?.id; + const issueNumber = issue?.number; + const repoFullName = repository?.full_name; + + const { org, repo } = getOrgAndRepoFromPath(repoFullName); + + if (!logMessage) return; + + if (typeof logMessage === "object") { + // pass log as json stringified + logMessage = JSON.stringify(logMessage); + } + + this.addToQueue({ repo, org, commentId, issueNumber, logMessage, level, timestamp }) + .then(() => { + return; + }) + .catch(() => { + console.log("Error adding logs to queue"); + }); + + if (this.logEnvironment === "development") { + console.log(this.app, logMessage, errorPayload, level, repo, org, commentId, issueNumber); + } + } + + info(message: string | object, errorPayload?: string | object) { + this.save(message, Level.INFO, errorPayload); + } + + warn(message: string | object, errorPayload?: string | object) { + this.save(message, Level.WARN, errorPayload); + } + + debug(message: string | object, errorPayload?: string | object) { + this.save(message, Level.DEBUG, errorPayload); + } + + error(message: string | object, errorPayload?: string | object) { + this.save(message, Level.ERROR, errorPayload); + } + + async get() { + try { + const { data, error } = await this.supabase.from("logs").select("*"); + + if (error) { + console.error("Error retrieving logs from Supabase:", error.message); + return []; + } + + return data; + } catch (error) { + if (error instanceof Error) { + // 👉️ err is type Error here + console.error("An error occurred:", error.message); + + return; + } + + console.log("Unexpected error", error); + return []; + } + } +} diff --git a/src/bindings/config.ts b/src/bindings/config.ts index c2a64e6b3..9fc6f4ed0 100644 --- a/src/bindings/config.ts +++ b/src/bindings/config.ts @@ -1,11 +1,19 @@ 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 { + DEFAULT_BOT_DELAY, + DEFAULT_DISQUALIFY_TIME, + DEFAULT_FOLLOWUP_TIME, + DEFAULT_PERMIT_BASE_URL, + DEFAULT_TIME_RANGE_FOR_MAX_ISSUE, + DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED, +} from "../configs"; import { getPayoutConfigByNetworkId } from "../helpers"; import { ajv } from "../utils"; import { Context } from "probot"; import { getScalarKey, getWideConfig } from "../utils/private"; +import { Level } from "../adapters/supabase"; export const loadConfig = async (context: Context): Promise => { const { @@ -13,8 +21,8 @@ export const loadConfig = async (context: Context): Promise => { timeLabels, privateKey, priorityLabels, - commentElementPricing, - autoPayMode, + incentives, + paymentPermitMaxPrice, disableAnalytics, bountyHunterMax, incentiveMode, @@ -22,6 +30,8 @@ export const loadConfig = async (context: Context): Promise => { issueCreatorMultiplier, defaultLabels, promotionComment, + commandSettings, + assistivePricing, registerWalletWithVerification, } = await getWideConfig(context); @@ -30,15 +40,16 @@ export const loadConfig = async (context: Context): Promise => { const botConfig: BotConfig = { log: { - level: process.env.LOG_LEVEL || "debug", - ingestionKey: process.env.LOGDNA_INGESTION_KEY ?? "", + logEnvironment: process.env.LOG_ENVIRONMENT || "production", + level: (process.env.LOG_LEVEL as Level) || Level.DEBUG, + retryLimit: Number(process.env.LOG_RETRY) || 0, }, price: { baseMultiplier, issueCreatorMultiplier, timeLabels, priorityLabels, - commentElementPricing, + incentives, defaultLabels, }, comments: { @@ -52,6 +63,12 @@ export const loadConfig = async (context: Context): Promise => { permitBaseUrl: process.env.PERMIT_BASE_URL || DEFAULT_PERMIT_BASE_URL, }, unassign: { + timeRangeForMaxIssue: process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE + ? Number(process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE) + : DEFAULT_TIME_RANGE_FOR_MAX_ISSUE, + timeRangeForMaxIssueEnabled: process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED + ? process.env.DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED == "true" + : DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED, followUpTime: ms(process.env.FOLLOW_UP_TIME || DEFAULT_FOLLOWUP_TIME), disqualifyTime: ms(process.env.DISQUALIFY_TIME || DEFAULT_DISQUALIFY_TIME), }, @@ -64,10 +81,12 @@ export const loadConfig = async (context: Context): Promise => { delay: process.env.TELEGRAM_BOT_DELAY ? Number(process.env.TELEGRAM_BOT_DELAY) : DEFAULT_BOT_DELAY, }, mode: { - autoPayMode: autoPayMode, + paymentPermitMaxPrice: paymentPermitMaxPrice, disableAnalytics: disableAnalytics, incentiveMode: incentiveMode, + assistivePricing: assistivePricing, }, + command: commandSettings, assign: { bountyHunterMax: bountyHunterMax, }, @@ -80,12 +99,8 @@ export const loadConfig = async (context: Context): Promise => { }, }; - if (botConfig.log.ingestionKey == "") { - throw new Error("LogDNA ingestion key missing"); - } - if (botConfig.payout.privateKey == "") { - botConfig.mode.autoPayMode = false; + botConfig.mode.paymentPermitMaxPrice = 0; } const validate = ajv.compile(BotConfigSchema); diff --git a/src/bindings/event.ts b/src/bindings/event.ts index 77518c99c..3ea42f287 100644 --- a/src/bindings/event.ts +++ b/src/bindings/event.ts @@ -1,5 +1,4 @@ import { Context } from "probot"; -import { createLogger } from "@logdna/logger"; import { createAdapters } from "../adapters"; import { processors, wildcardProcessors } from "../handlers/processors"; import { shouldSkip } from "../helpers"; @@ -7,6 +6,7 @@ import { BotConfig, GithubEvent, Payload, PayloadSchema } from "../types"; import { Adapters } from "../types/adapters"; import { ajv } from "../utils"; import { loadConfig } from "./config"; +import { GitHubLogger } from "../adapters/supabase"; let botContext: Context = {} as Context; export const getBotContext = () => botContext; @@ -23,6 +23,7 @@ export type Logger = { warn: (msg: string | object, options?: JSON) => void; error: (msg: string | object, options?: JSON) => void; }; + let logger: Logger; export const getLogger = (): Logger => logger; @@ -35,15 +36,21 @@ export const bindEvents = async (context: Context): Promise => { botConfig = await loadConfig(context); + adapters = createAdapters(botConfig); + const options = { app: "UbiquiBot", - level: botConfig.log.level, + // level: botConfig.log.level, }; - logger = createLogger(botConfig.log.ingestionKey, options) as Logger; + + logger = new GitHubLogger(options.app, botConfig.log.logEnvironment, botConfig.log.level, botConfig.log.retryLimit); // contributors will see logs in console while on development env if (!logger) { return; } + // Create adapters for telegram, supabase, twitter, discord, etc + logger.info("Creating adapters for supabase, telegram, twitter, etc..."); + logger.info( `Config loaded! config: ${JSON.stringify({ price: botConfig.price, @@ -64,10 +71,6 @@ export const bindEvents = async (context: Context): Promise => { return; } - // Create adapters for telegram, supabase, twitter, discord, etc - logger.info("Creating adapters for supabase, telegram, twitter, etc..."); - adapters = createAdapters(botConfig); - // Skip validation for installation event and push if (!NO_VALIDATION.includes(eventName)) { // Validate payload @@ -112,8 +115,8 @@ export const bindEvents = async (context: Context): Promise => { await postAction(); } - // Skip wildcard handlers for installation event - if (eventName !== GithubEvent.INSTALLATION_ADDED_EVENT) { + // Skip wildcard handlers for installation event and push event + if (eventName !== GithubEvent.INSTALLATION_ADDED_EVENT && eventName !== GithubEvent.PUSH_EVENT) { // Run wildcard handlers logger.info(`Running wildcard handlers: ${wildcardProcessors.map((fn) => fn.name)}`); for (const wildcardProcessor of wildcardProcessors) { diff --git a/src/configs/shared.ts b/src/configs/shared.ts index 2d817b7a0..8c287c4ff 100644 --- a/src/configs/shared.ts +++ b/src/configs/shared.ts @@ -5,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 diff --git a/src/configs/strings.ts b/src/configs/strings.ts index 9bc705a20..3f3128920 100644 --- a/src/configs/strings.ts +++ b/src/configs/strings.ts @@ -2,4 +2,8 @@ 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.", + skipPriceLabelGenerationComment: "Pricing is disabled on parent issues.", + ignoreStartCommandForParentIssueComment: + "Please select a child issue from the specification checklist to work on. The `/start` command is disabled on parent issues.", + autopayComment: "Automatic payment for this issue is enabled:", }; diff --git a/src/handlers/assign/action.ts b/src/handlers/assign/action.ts index bd71d65c3..b42312a88 100644 --- a/src/handlers/assign/action.ts +++ b/src/handlers/assign/action.ts @@ -1,5 +1,5 @@ import { getBotConfig, getBotContext, getLogger } from "../../bindings"; -import { addCommentToIssue, closePullRequest, getOpenedPullRequestsForAnIssue } from "../../helpers"; +import { addCommentToIssue, closePullRequest, getOpenedPullRequestsForAnIssue, calculateWeight, calculateDuration } from "../../helpers"; import { Payload, LabelItem } from "../../types"; import { deadLinePrefix } from "../shared"; @@ -49,9 +49,9 @@ export const commentWithAssignMessage = async (): Promise => { return; } - const sorted = timeLabelsAssigned.sort((a, b) => a.weight - b.weight); + const sorted = timeLabelsAssigned.sort((a, b) => calculateWeight(a) - calculateWeight(b)); const targetTimeLabel = sorted[0]; - const duration = targetTimeLabel.value; + const duration = calculateDuration(targetTimeLabel); if (!duration) { logger.debug(`Missing configure for timelabel: ${targetTimeLabel.name}`); return; diff --git a/src/handlers/assign/auto.ts b/src/handlers/assign/auto.ts index a065327f4..7c648b5e7 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, getPullByNumber, getPullRequests } from "../../helpers"; +import { addAssignees, getAllPullRequests, getIssueByNumber, getPullByNumber } from "../../helpers"; import { gitLinkedIssueParser } from "../../helpers/parser"; import { Payload } from "../../types"; @@ -7,7 +7,7 @@ import { Payload } from "../../types"; export const checkPullRequests = async () => { const context = getBotContext(); const logger = getLogger(); - const pulls = await getPullRequests(context); + const pulls = await getAllPullRequests(context); if (pulls.length === 0) { logger.debug(`No pull requests found at this time`); diff --git a/src/handlers/comment/action.ts b/src/handlers/comment/action.ts index 1857b7851..84c54e6d5 100644 --- a/src/handlers/comment/action.ts +++ b/src/handlers/comment/action.ts @@ -1,10 +1,12 @@ -import { getBotContext, getLogger } from "../../bindings"; +import { getBotConfig, getBotContext, getLogger } from "../../bindings"; import { Payload } from "../../types"; +import { IssueCommentCommands } from "./commands"; import { commentParser, userCommands } from "./handlers"; import { verifyFirstCheck } from "./handlers/first"; export const handleComment = async (): Promise => { const context = getBotContext(); + const config = getBotConfig(); const logger = getLogger(); const payload = context.payload as Payload; @@ -28,13 +30,21 @@ export const handleComment = async (): Promise => { const userCommand = allCommands.find((i) => i.id == command); if (userCommand) { - const { handler, callback, successComment, failureComment } = userCommand; + const { id, handler, callback, successComment, failureComment } = userCommand; logger.info(`Running a comment handler: ${handler.name}`); const { payload: _payload } = getBotContext(); const issue = (_payload as Payload).issue; if (!issue) continue; + const feature = config.command.find((e) => e.name === id.split("/")[1]); + + if (!feature?.enabled && id !== IssueCommentCommands.HELP) { + logger.info(`Skipping '${id}' because it is disabled on this repo`); + await callback(issue.number, `Skipping \`${id}\` because it is disabled on this repo`, payload.action, payload.comment); + continue; + } + try { const response = await handler(body); const callbackComment = response ?? successComment ?? ""; diff --git a/src/handlers/comment/commands.ts b/src/handlers/comment/commands.ts index 878727dcb..afdab0b03 100644 --- a/src/handlers/comment/commands.ts +++ b/src/handlers/comment/commands.ts @@ -1,7 +1,7 @@ export enum IssueCommentCommands { HELP = "/help", // list available commands - ASSIGN = "/assign", // assign the hunter to the issue automatically - UNASSIGN = "/unassign", // unassign to default + START = "/start", // assign the hunter to the issue automatically + STOP = "/stop", // unassign to default WALLET = "/wallet", // register wallet address PAYOUT = "/payout", // request permit payout MULTIPLIER = "/multiplier", // set bounty multiplier (for treasury) @@ -9,4 +9,5 @@ export enum IssueCommentCommands { // Access Controls ALLOW = "/allow", + AUTOPAY = "/autopay", } diff --git a/src/handlers/comment/handlers/allow.ts b/src/handlers/comment/handlers/allow.ts index 52d22e9bc..978dbbef4 100644 --- a/src/handlers/comment/handlers/allow.ts +++ b/src/handlers/comment/handlers/allow.ts @@ -20,7 +20,7 @@ export const setAccess = async (body: string) => { return; } - const regex = /^\/allow set-(\S+)\s@(\w+)\s(true|false)$/; + const regex = /^\/allow\s+set-(\S+)\s+@([\w-]+)\s+(true|false)\s*$/; const matches = body.match(regex); diff --git a/src/handlers/comment/handlers/assign.ts b/src/handlers/comment/handlers/assign.ts index 5b59add42..5394b3404 100644 --- a/src/handlers/comment/handlers/assign.ts +++ b/src/handlers/comment/handlers/assign.ts @@ -1,11 +1,12 @@ -import { addAssignees, getAssignedIssues, getAvailableOpenedPullRequests, getAllIssueComments } from "../../../helpers"; +import { addAssignees, getAssignedIssues, getAvailableOpenedPullRequests, getAllIssueComments, calculateWeight, calculateDuration } from "../../../helpers"; import { getBotConfig, getBotContext, getLogger } from "../../../bindings"; -import { Payload, LabelItem, Comment, IssueType } from "../../../types"; +import { Payload, LabelItem, Comment, IssueType, Issue } from "../../../types"; import { deadLinePrefix } from "../../shared"; import { getWalletAddress, getWalletMultiplier } from "../../../adapters/supabase"; import { tableComment } from "./table"; import { bountyInfo } from "../../wildcard"; import { ASSIGN_COMMAND_ENABLED, GLOBAL_STRINGS } from "../../../configs"; +import { isParentIssue } from "../../pricing"; export const assign = async (body: string) => { const { payload: _payload } = getBotContext(); @@ -17,18 +18,23 @@ export const assign = async (body: string) => { const id = organization?.id || repository?.id; // repository?.id as fallback - logger.info(`Received '/assign' command from user: ${payload.sender.login}, body: ${body}`); + logger.info(`Received '/start' command from user: ${payload.sender.login}, body: ${body}`); const issue = (_payload as Payload).issue; if (!issue) { - logger.info(`Skipping '/assign' because of no issue instance`); - return "Skipping '/assign' because of no issue instance"; + logger.info(`Skipping '/start' because of no issue instance`); + return "Skipping '/start' because of no issue instance"; } if (!ASSIGN_COMMAND_ENABLED) { - logger.info(`Ignore '/assign' command from user: ASSIGN_COMMAND_ENABLED config is set false`); + logger.info(`Ignore '/start' command from user: ASSIGN_COMMAND_ENABLED config is set false`); return GLOBAL_STRINGS.assignCommandDisabledComment; } + if (issue.body && isParentIssue(issue.body)) { + logger.info(`Ignore '/start' command from user: identified as parent issue`); + return GLOBAL_STRINGS.ignoreStartCommandForParentIssueComment; + } + 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)}`); @@ -41,22 +47,22 @@ export const assign = async (body: string) => { } if (issue.state == IssueType.CLOSED) { - logger.info("Skipping '/assign', reason: closed "); - return "Skipping `/assign` since the issue is closed"; + logger.info("Skipping '/start', reason: closed "); + return "Skipping `/start` since the issue is closed"; } const _assignees = payload.issue?.assignees; const assignees = _assignees ?? []; if (assignees.length !== 0) { - logger.info(`Skipping '/assign', reason: already assigned. assignees: ${assignees.length > 0 ? assignees.map((i) => i.login).join() : "NoAssignee"}`); - return "Skipping `/assign` since the issue is already assigned"; + logger.info(`Skipping '/start', reason: already assigned. assignees: ${assignees.length > 0 ? assignees.map((i) => i.login).join() : "NoAssignee"}`); + return "Skipping `/start` since the issue is already assigned"; } // get the time label from the `labels` const labels = payload.issue?.labels; if (!labels) { logger.info(`No labels to calculate timeline`); - return "Skipping `/assign` since no issue labels are set to calculate the timeline"; + return "Skipping `/start` since no issue labels are set to calculate the timeline"; } const timeLabelsDefined = config.price.timeLabels; const timeLabelsAssigned: LabelItem[] = []; @@ -72,30 +78,23 @@ export const assign = async (body: string) => { if (timeLabelsAssigned.length == 0) { logger.info(`No time labels to calculate timeline`); - return "Skipping `/assign` since no time labels are set to calculate the timeline"; + return "Skipping `/start` since no time labels are set to calculate the timeline"; } - const sorted = timeLabelsAssigned.sort((a, b) => a.weight - b.weight); + const sorted = timeLabelsAssigned.sort((a, b) => calculateWeight(a) - calculateWeight(b)); const targetTimeLabel = sorted[0]; - const duration = targetTimeLabel.value; + const duration = calculateDuration(targetTimeLabel); if (!duration) { logger.info(`Missing configure for time label: ${targetTimeLabel.name}`); - return "Skipping `/assign` since configuration is missing for the following labels"; + return "Skipping `/start` since configuration is missing for the following labels"; } const startTime = new Date().getTime(); const endTime = new Date(startTime + duration * 1000); - const { reason, value } = await getWalletMultiplier(payload.sender.login, id?.toString()); - - const multiplier = value?.toFixed(2) || "1.00"; - 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, - reason, - bounty: `Permit generation skipped since price label is not set`, commit: `@${payload.sender.login} ${deadLinePrefix} ${endTime.toUTCString()}`, tips: `
Tips:
    @@ -116,11 +115,35 @@ export const assign = async (body: string) => { 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, reason, bounty } = await getMultiplierInfoToDisplay(payload.sender.login, id?.toString(), issue); + return tableComment({ ...comment, multiplier, reason, bounty }) + comment.tips; + } + return; +}; + +const getMultiplierInfoToDisplay = async (senderLogin: string, org_id: string, issue: Issue) => { + const { reason, value } = await getWalletMultiplier(senderLogin, org_id); + + const multiplier = value?.toFixed(2) || "1.00"; + + let _multiplierToDisplay, _reasonToDisplay, _bountyToDisplay; + + if (value == 1) { + if (reason) { + _multiplierToDisplay = multiplier; + _reasonToDisplay = reason; + } else { + // default mode: normal bounty hunter with default multiplier 1 and no reason + // nothing to show about multiplier + } + } else { + _multiplierToDisplay = multiplier; + _reasonToDisplay = reason; + _bountyToDisplay = `Permit generation skipped since price label is not set`; const issueDetailed = bountyInfo(issue); if (issueDetailed.priceLabel) { - comment.bounty = (+issueDetailed.priceLabel.substring(7, issueDetailed.priceLabel.length - 4) * value).toString() + " USD"; + _bountyToDisplay = (+issueDetailed.priceLabel.substring(7, issueDetailed.priceLabel.length - 4) * value).toString() + " USD"; } - return tableComment(comment) + comment.tips; } - return; + return { multiplier: _multiplierToDisplay, reason: _reasonToDisplay, bounty: _bountyToDisplay }; }; diff --git a/src/handlers/comment/handlers/help.ts b/src/handlers/comment/handlers/help.ts index 9c461e84a..7e2c11bd4 100644 --- a/src/handlers/comment/handlers/help.ts +++ b/src/handlers/comment/handlers/help.ts @@ -20,7 +20,7 @@ export const listAvailableCommands = async (body: string) => { } if (issue.state == IssueType.CLOSED) { - logger.info("Skipping '/assign', reason: closed "); + logger.info("Skipping '/start', reason: closed "); return; } diff --git a/src/handlers/comment/handlers/index.ts b/src/handlers/comment/handlers/index.ts index 24d5fa635..5470b3062 100644 --- a/src/handlers/comment/handlers/index.ts +++ b/src/handlers/comment/handlers/index.ts @@ -20,10 +20,13 @@ import { getPayoutConfigByNetworkId, getTokenSymbol, getAllIssueAssignEvents, + calculateWeight, } from "../../../helpers"; import { getBotConfig, getBotContext, getLogger } from "../../../bindings"; import { handleIssueClosed } from "../../payout"; import { query } from "./query"; +import { autoPay } from "./payout"; +import { getTargetPriceLabel } from "../../shared"; export * from "./assign"; export * from "./wallet"; @@ -77,24 +80,39 @@ export const issueClosedCallback = async (): Promise => { */ export const issueCreatedCallback = async (): Promise => { + const logger = getLogger(); const { payload: _payload } = getBotContext(); const config = getBotConfig(); const issue = (_payload as Payload).issue; if (!issue) return; const labels = issue.labels; + + const { assistivePricing } = config.mode; + + if (!assistivePricing) { + logger.info("Skipping adding label to issue because assistive pricing is disabled"); + return; + } + try { 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 && priorityLabels.length === 0) { - for (const label of config.price.defaultLabels) { - const exists = await getLabel(label); - if (!exists) await createLabel(label); - await addLabelToIssue(label); - } + const minTimeLabel = + timeLabels.length > 0 ? timeLabels.reduce((a, b) => (calculateWeight(a) < calculateWeight(b) ? a : b)).name : config.price.defaultLabels[0]; + const minPriorityLabel = + priorityLabels.length > 0 ? priorityLabels.reduce((a, b) => (calculateWeight(a) < calculateWeight(b) ? a : b)).name : config.price.defaultLabels[1]; + if (!timeLabels.length) await addLabelToIssue(minTimeLabel); + if (!priorityLabels.length) await addLabelToIssue(minPriorityLabel); + + const targetPriceLabel = getTargetPriceLabel(minTimeLabel, minPriorityLabel); + if (targetPriceLabel && !labels.map((i) => i.name).includes(targetPriceLabel)) { + const exist = await getLabel(targetPriceLabel); + if (!exist) await createLabel(targetPriceLabel, "price"); + await addLabelToIssue(targetPriceLabel); } } catch (err: unknown) { - return await addCommentToIssue(`Error: ${err}`, issue.number); + await addCommentToIssue(`Error: ${err}`, issue.number); } }; @@ -192,13 +210,13 @@ export const userCommands = (): UserCommands[] => { return [ { - id: IssueCommentCommands.ASSIGN, + id: IssueCommentCommands.START, description: "Assign the origin sender to the issue automatically.", handler: assign, callback: commandCallback, }, { - id: IssueCommentCommands.UNASSIGN, + id: IssueCommentCommands.STOP, description: "Unassign the origin sender from the issue automatically.", handler: unassign, callback: commandCallback, @@ -216,6 +234,12 @@ export const userCommands = (): UserCommands[] => { handler: payout, callback: commandCallback, },*/ + { + id: IssueCommentCommands.AUTOPAY, + description: "Toggle automatic payment for the completion of the current issue.", + handler: autoPay, + callback: commandCallback, + }, { id: IssueCommentCommands.QUERY, description: `Comments the users multiplier and address`, diff --git a/src/handlers/comment/handlers/multiplier.ts b/src/handlers/comment/handlers/multiplier.ts index 84a3e88bf..a7cc674eb 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) => { } else if (part.startsWith("@")) { username = part.substring(1); } else { - reason += part.replace(/['"]/g, "") + " "; + reason += part.replace(/['"]/g, ""); } } username = username || sender; @@ -68,13 +68,18 @@ export const multiplier = async (body: string) => { return "Insufficient permissions to update the payout multiplier. You are not an `admin` or `billing_manager`"; } } - logger.info(`Upserting to the wallet table, username: ${username}, bountyMultiplier: ${bountyMultiplier}, reason: ${reason}}`); await upsertWalletMultiplier(username, bountyMultiplier?.toString(), reason, id?.toString()); - return `Successfully changed the payout multiplier for @${username} to ${bountyMultiplier}. The reason ${ - reason ? `provided is "${reason}"` : "is not provided" - }.`; + if (bountyMultiplier > 1) { + return `Successfully changed the payout multiplier for @${username} to ${bountyMultiplier}. The reason ${ + reason ? `provided is "${reason}"` : "is not provided" + }. This feature is designed to limit the contributor's compensation for any bounty on the current repository due to other compensation structures (i.e. salary.) are you sure you want to use a bounty multiplier above 1?`; + } else { + 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 syntax for wallet command \n example usage: "/multiplier @user 0.5 'Multiplier reason'"`; diff --git a/src/handlers/comment/handlers/payout.ts b/src/handlers/comment/handlers/payout.ts index da07f5fdb..98ce9f9ae 100644 --- a/src/handlers/comment/handlers/payout.ts +++ b/src/handlers/comment/handlers/payout.ts @@ -2,7 +2,8 @@ import { getBotContext, getLogger } from "../../../bindings"; import { Payload } from "../../../types"; import { IssueCommentCommands } from "../commands"; import { handleIssueClosed } from "../../payout"; -import { getAllIssueComments } from "../../../helpers"; +import { getAllIssueComments, getUserPermission } from "../../../helpers"; +import { GLOBAL_STRINGS } from "../../../configs"; export const payout = async (body: string) => { const { payload: _payload } = getBotContext(); @@ -40,3 +41,26 @@ export const payout = async (body: string) => { const response = await handleIssueClosed(); return response; }; + +export const autoPay = async (body: string) => { + const context = getBotContext(); + const _payload = context.payload; + const logger = getLogger(); + + const payload = _payload as Payload; + logger.info(`Received '/autopay' command from user: ${payload.sender.login}`); + + const pattern = /^\/autopay (true|false)$/; + const res = body.match(pattern); + + if (res) { + const userPermission = await getUserPermission(payload.sender.login, context); + if (userPermission !== "admin" && userPermission !== "billing_manager") { + return "You must be an `admin` or `billing_manager` to toggle automatic payments for completed issues."; + } + if (res.length > 1) { + return `${GLOBAL_STRINGS.autopayComment} **${res[1]}**`; + } + } + return "Invalid body for autopay command: e.g. /autopay false"; +}; diff --git a/src/handlers/comment/handlers/query.ts b/src/handlers/comment/handlers/query.ts index ea3317981..efee499d1 100644 --- a/src/handlers/comment/handlers/query.ts +++ b/src/handlers/comment/handlers/query.ts @@ -19,7 +19,7 @@ export const query = async (body: string) => { return `Skipping '/query' because of no issue instance`; } - const regex = /^\/query\s@(\w+)$/; + const regex = /^\/query\s+@([\w-]+)\s*$/; const matches = body.match(regex); const user = matches?.[1]; diff --git a/src/handlers/comment/handlers/table.ts b/src/handlers/comment/handlers/table.ts index fef8e181e..13bed9e88 100644 --- a/src/handlers/comment/handlers/table.ts +++ b/src/handlers/comment/handlers/table.ts @@ -7,9 +7,9 @@ export const tableComment = ({ }: { deadline: string; wallet: string; - multiplier: string; - reason: string; - bounty: string; + multiplier?: string; + reason?: string; + bounty?: string; }) => { return ` @@ -27,18 +27,9 @@ export const tableComment = ({ Registered Wallet ${wallet} - - Payment Multiplier - ${multiplier} - - - Multiplier Reason - ${reason} - - - Total Bounty - ${bounty} - + ${multiplier ? `Payment Multiplier${multiplier}` : ``} + ${reason ? `Multiplier Reason${reason}` : ``} + ${bounty ? `Total Bounty${bounty}` : ``} `; }; diff --git a/src/handlers/comment/handlers/unassign.ts b/src/handlers/comment/handlers/unassign.ts index 3289ed0b0..91aae184e 100644 --- a/src/handlers/comment/handlers/unassign.ts +++ b/src/handlers/comment/handlers/unassign.ts @@ -7,16 +7,16 @@ import { closePullRequestForAnIssue } from "../../assign/index"; export const unassign = async (body: string) => { const { payload: _payload } = getBotContext(); const logger = getLogger(); - if (body != IssueCommentCommands.UNASSIGN && body.replace(/`/g, "") != IssueCommentCommands.UNASSIGN) { + if (body != IssueCommentCommands.STOP && body.replace(/`/g, "") != IssueCommentCommands.STOP) { logger.info(`Skipping to unassign. body: ${body}`); return; } const payload = _payload as Payload; - logger.info(`Received '/unassign' command from user: ${payload.sender.login}`); + logger.info(`Received '/stop' command from user: ${payload.sender.login}`); const issue = (_payload as Payload).issue; if (!issue) { - logger.info(`Skipping '/unassign' because of no issue instance`); + logger.info(`Skipping '/stop' because of no issue instance`); return; } diff --git a/src/handlers/issue/index.ts b/src/handlers/issue/index.ts new file mode 100644 index 000000000..12e1212ae --- /dev/null +++ b/src/handlers/issue/index.ts @@ -0,0 +1 @@ +export * from "./pre"; diff --git a/src/handlers/issue/pre.ts b/src/handlers/issue/pre.ts new file mode 100644 index 000000000..bebda5dce --- /dev/null +++ b/src/handlers/issue/pre.ts @@ -0,0 +1,49 @@ +import { extractImportantWords, upsertCommentToIssue, measureSimilarity } from "../../helpers"; +import { getBotContext, getLogger } from "../../bindings"; +import { Issue, Payload } from "../../types"; + +export const findDuplicateOne = async () => { + const logger = getLogger(); + const context = getBotContext(); + const payload = context.payload as Payload; + const issue = payload.issue; + + if (!issue?.body) return; + const importantWords = await extractImportantWords(issue); + const perPage = 10; + let curPage = 1; + + for (const importantWord of importantWords) { + let fetchDone = false; + try { + while (!fetchDone) { + const response = await context.octokit.rest.search.issuesAndPullRequests({ + q: `${importantWord} repo:${payload.repository.owner.login}/${payload.repository.name} is:issue`, + sort: "created", + order: "desc", + per_page: perPage, + page: curPage, + }); + if (response.data.items.length > 0) { + for (const result of response.data.items) { + if (!result.body) continue; + if (result.id === issue.id) continue; + const similarity = await measureSimilarity(issue, result as Issue); + if (similarity > parseInt(process.env.SIMILARITY_THRESHOLD || "80")) { + await upsertCommentToIssue( + issue.number, + `Similar issue (${result.title}) found at ${result.html_url}.\nSimilarity is about ${similarity}%`, + "created" + ); + return; + } + } + } + if (response.data.items.length < perPage) fetchDone = true; + else curPage++; + } + } catch (e: unknown) { + logger.error(`Could not find any issues, reason: ${e}`); + } + } +}; diff --git a/src/handlers/payout/action.ts b/src/handlers/payout/action.ts index ba36fd52e..fd2aaaaac 100644 --- a/src/handlers/payout/action.ts +++ b/src/handlers/payout/action.ts @@ -3,22 +3,28 @@ import { getPenalty, getWalletAddress, getWalletMultiplier, removePenalty } from import { getBotConfig, getBotContext, getLogger } from "../../bindings"; import { addLabelToIssue, + checkUserPermissionForRepoAndOrg, + clearAllPriceLabelsOnIssue, deleteLabel, generatePermit2Signature, - getAllIssueAssignEvents, getAllIssueComments, getTokenSymbol, + savePermitToDB, wasIssueReopened, + getAllIssueAssignEvents, } from "../../helpers"; import { UserType, Payload, StateReason } from "../../types"; import { shortenEthAddress } from "../../utils"; import { bountyInfo } from "../wildcard"; +import Decimal from "decimal.js"; +import { GLOBAL_STRINGS } from "../../configs"; +import { isParentIssue } from "../pricing"; export const handleIssueClosed = async () => { const context = getBotContext(); const { - payout: { paymentToken, rpc, permitBaseUrl, networkId }, - mode: { autoPayMode }, + payout: { paymentToken, rpc, permitBaseUrl, networkId, privateKey }, + mode: { paymentPermitMaxPrice }, } = getBotConfig(); const logger = getLogger(); const payload = context.payload as Payload; @@ -29,11 +35,15 @@ export const handleIssueClosed = async () => { if (!issue) return; + const userHasPermission = await checkUserPermissionForRepoAndOrg(payload.sender.login, context); + + if (!userHasPermission) return "Permit generation skipped because this issue has been closed by an external contributor."; + const comments = await getAllIssueComments(issue.number); const wasReopened = await wasIssueReopened(issue.number); const claimUrlRegex = new RegExp(`\\((${permitBaseUrl}\\?claim=\\S+)\\)`); - const permitCommentIdx = comments.findIndex((e) => e.user.type === "Bot" && e.body.match(claimUrlRegex)); + const permitCommentIdx = comments.findIndex((e) => e.user.type === UserType.Bot && e.body.match(claimUrlRegex)); if (wasReopened && permitCommentIdx !== -1) { const permitComment = comments[permitCommentIdx]; @@ -80,17 +90,43 @@ export const handleIssueClosed = async () => { logger.info(`Penalty removed`); return; } - + if (privateKey == "") { + logger.info("Permit generation skipped because wallet private key is not set"); + return "Permit generation skipped because wallet private key is not set"; + } if (issue.state_reason !== StateReason.COMPLETED) { logger.info("Permit generation skipped because the issue was not closed as completed"); return "Permit generation skipped because the issue was not closed as completed"; } + logger.info(`Checking if the issue is a parent issue.`); + if (issue.body && isParentIssue(issue.body)) { + logger.error("Permit generation skipped since the issue is identified as parent issue."); + await clearAllPriceLabelsOnIssue(); + return "Permit generation skipped since the issue is identified as parent issue."; + } + logger.info(`Handling issues.closed event, issue: ${issue.number}`); - if (!autoPayMode) { - logger.info(`Skipping to generate permit2 url, reason: { autoPayMode: ${autoPayMode}}`); - return `Permit generation skipped since autoPayMode is disabled`; + for (const botComment of comments.filter((cmt) => cmt.user.type === UserType.Bot).reverse()) { + const botCommentBody = botComment.body; + if (botCommentBody.includes(GLOBAL_STRINGS.autopayComment)) { + const pattern = /\*\*(\w+)\*\*/; + const res = botCommentBody.match(pattern); + if (res) { + if (res[1] === "false") { + logger.info(`Skipping to generate permit2 url, reason: autoPayMode for this issue: false`); + return `Permit generation skipped since automatic payment for this issue is disabled.`; + } + break; + } + } + } + + if (paymentPermitMaxPrice == 0 || !paymentPermitMaxPrice) { + logger.info(`Skipping to generate permit2 url, reason: { paymentPermitMaxPrice: ${paymentPermitMaxPrice}}`); + return `Permit generation skipped since paymentPermitMaxPrice is 0`; } + const issueDetailed = bountyInfo(issue); if (!issueDetailed.isBounty) { logger.info(`Skipping... its not a bounty`); @@ -110,26 +146,30 @@ export const handleIssueClosed = async () => { } const recipient = await getWalletAddress(assignee.login); - const { value } = await getWalletMultiplier(assignee.login, id?.toString()); + if (!recipient || recipient?.trim() === "") { + logger.info(`Recipient address is missing`); + return; + } + + const { value: multiplier } = await getWalletMultiplier(assignee.login, id?.toString()); - if (value === 0) { + if (multiplier === 0) { const errMsg = "Refusing to generate the payment permit because " + `@${assignee.login}` + "'s payment `multiplier` is `0`"; logger.info(errMsg); return errMsg; } - // TODO: add multiplier to the priceInEth - let priceInEth = (+issueDetailed.priceLabel.substring(7, issueDetailed.priceLabel.length - 4) * value).toString(); - if (!recipient || recipient?.trim() === "") { - logger.info(`Recipient address is missing`); - return; + let priceInEth = new Decimal(issueDetailed.priceLabel.substring(7, issueDetailed.priceLabel.length - 4)).mul(multiplier); + if (priceInEth.gt(paymentPermitMaxPrice)) { + logger.info("Skipping to proceed the payment because bounty payout is higher than paymentPermitMaxPrice"); + return `Permit generation skipped since issue's bounty is higher than ${paymentPermitMaxPrice}`; } // if bounty hunter has any penalty then deduct it from the bounty const penaltyAmount = await getPenalty(assignee.login, payload.repository.full_name, paymentToken, networkId.toString()); if (penaltyAmount.gt(0)) { logger.info(`Deducting penalty from bounty`); - const bountyAmount = ethers.utils.parseUnits(priceInEth, 18); + const bountyAmount = ethers.utils.parseUnits(priceInEth.toString(), 18); const bountyAmountAfterPenalty = bountyAmount.sub(penaltyAmount); if (bountyAmountAfterPenalty.lte(0)) { await removePenalty(assignee.login, payload.repository.full_name, paymentToken, networkId.toString(), bountyAmount); @@ -137,14 +177,15 @@ export const handleIssueClosed = async () => { logger.info(msg); return msg; } - priceInEth = ethers.utils.formatUnits(bountyAmountAfterPenalty, 18); + priceInEth = new Decimal(ethers.utils.formatUnits(bountyAmountAfterPenalty, 18)); } - const payoutUrl = await generatePermit2Signature(recipient, priceInEth, issue.node_id); + const { txData, payoutUrl } = await generatePermit2Signature(recipient, priceInEth, issue.node_id); const tokenSymbol = await getTokenSymbol(paymentToken, rpc); const shortenRecipient = shortenEthAddress(recipient, `[ CLAIM ${priceInEth} ${tokenSymbol.toUpperCase()} ]`.length); logger.info(`Posting a payout url to the issue, url: ${payoutUrl}`); - const comment = `### [ **[ CLAIM ${priceInEth} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n` + "```" + shortenRecipient + "```"; + const comment = + `#### Task Assignee Reward\n### [ **[ CLAIM ${priceInEth} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n` + "```" + shortenRecipient + "```"; 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`); @@ -152,6 +193,7 @@ export const handleIssueClosed = async () => { } await deleteLabel(issueDetailed.priceLabel); await addLabelToIssue("Permitted"); + await savePermitToDB(assignee.id, txData); if (penaltyAmount.gt(0)) { await removePenalty(assignee.login, payload.repository.full_name, paymentToken, networkId.toString(), penaltyAmount); } diff --git a/src/handlers/payout/post.ts b/src/handlers/payout/post.ts index c4d80b408..200fe5131 100644 --- a/src/handlers/payout/post.ts +++ b/src/handlers/payout/post.ts @@ -1,7 +1,10 @@ import { getWalletAddress } from "../../adapters/supabase"; import { getBotConfig, getBotContext, getLogger } from "../../bindings"; import { addCommentToIssue, generatePermit2Signature, getAllIssueComments, getIssueDescription, getTokenSymbol, parseComments } from "../../helpers"; -import { MarkdownItem, Payload, UserType, CommentElementPricing } from "../../types"; +import { Incentives, MarkdownItem, Payload, StateReason, UserType } from "../../types"; +import { commentParser } from "../comment"; +import Decimal from "decimal.js"; +import { bountyInfo } from "../wildcard"; const ItemsToExclude: string[] = [MarkdownItem.BlockQuote]; /** @@ -11,8 +14,8 @@ const ItemsToExclude: string[] = [MarkdownItem.BlockQuote]; export const incentivizeComments = async () => { const logger = getLogger(); const { - mode: { incentiveMode }, - price: { baseMultiplier, commentElementPricing }, + mode: { incentiveMode, paymentPermitMaxPrice }, + price: { baseMultiplier, incentives }, payout: { paymentToken, rpc }, } = getBotConfig(); if (!incentiveMode) { @@ -21,26 +24,63 @@ export const incentivizeComments = async () => { } const context = getBotContext(); const payload = context.payload as Payload; - const org = payload.organization?.login; const issue = payload.issue; - if (!issue || !org) { - logger.info(`Incomplete payload. issue: ${issue}, org: ${org}`); + if (!issue) { + logger.info(`Incomplete payload. issue: ${issue}`); return; } + + if (issue.state_reason !== StateReason.COMPLETED) { + logger.info("incentivizeComments: comment incentives skipped because the issue was not closed as completed"); + return; + } + + if (!paymentPermitMaxPrice) { + logger.info(`incentivizeComments: skipping to generate permit2 url, reason: { paymentPermitMaxPrice: ${paymentPermitMaxPrice}}`); + return; + } + + const issueDetailed = bountyInfo(issue); + if (!issueDetailed.isBounty) { + logger.info(`incentivizeComments: its not a bounty`); + return; + } + + const comments = await getAllIssueComments(issue.number); + const permitComments = comments.filter( + (content) => content.body.includes("Conversation Rewards") && content.body.includes("https://pay.ubq.fi?claim=") && content.user.type == UserType.Bot + ); + if (permitComments.length > 0) { + logger.info(`incentivizeComments: skip to generate a permit url because it has been already posted`); + return; + } + const assignees = issue?.assignees ?? []; const assignee = assignees.length > 0 ? assignees[0] : undefined; if (!assignee) { - logger.info("Skipping payment permit generation because `assignee` is `undefined`."); + logger.info("incentivizeComments: skipping payment permit generation because `assignee` is `undefined`."); return; } - const issueComments = await getAllIssueComments(issue.number); + const issueComments = await getAllIssueComments(issue.number, "full"); logger.info(`Getting the issue comments done. comments: ${JSON.stringify(issueComments)}`); const issueCommentsByUser: Record = {}; for (const issueComment of issueComments) { const user = issueComment.user; if (user.type == UserType.Bot || user.login == assignee) continue; - issueCommentsByUser[user.login].push(issueComment.body); + const commands = commentParser(issueComment.body); + if (commands.length > 0) { + logger.info(`Skipping to parse the comment because it contains commands. comment: ${JSON.stringify(issueComment)}`); + continue; + } + if (!issueComment.body_html) { + logger.info(`Skipping to parse the comment because body_html is undefined. comment: ${JSON.stringify(issueComment)}`); + continue; + } + if (!issueCommentsByUser[user.login]) { + issueCommentsByUser[user.login] = []; + } + issueCommentsByUser[user.login].push(issueComment.body_html); } const tokenSymbol = await getTokenSymbol(paymentToken, rpc); logger.info(`Filtering by the user type done. commentsByUser: ${JSON.stringify(issueCommentsByUser)}`); @@ -49,17 +89,25 @@ export const incentivizeComments = async () => { const reward: Record = {}; // The mapping between gh handle and amount in ETH - const fallbackReward: Record = {}; - let comment = ""; + const fallbackReward: Record = {}; + let comment = `#### Conversation Rewards\n`; for (const user of Object.keys(issueCommentsByUser)) { const comments = issueCommentsByUser[user]; const commentsByNode = await parseComments(comments, ItemsToExclude); - const rewardValue = calculateRewardValue(commentsByNode, commentElementPricing); + const rewardValue = calculateRewardValue(commentsByNode, incentives); + if (rewardValue.equals(0)) { + logger.info(`Skipping to generate a permit url because the reward value is 0. user: ${user}`); + continue; + } logger.debug(`Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`); const account = await getWalletAddress(user); - const amountInETH = ((rewardValue * baseMultiplier) / 1000).toString(); + const amountInETH = rewardValue.mul(baseMultiplier); + if (amountInETH.gt(paymentPermitMaxPrice)) { + logger.info(`Skipping comment reward for user ${user} because reward is higher than payment permit max price`); + continue; + } if (account) { - const payoutUrl = await generatePermit2Signature(account, amountInETH, issue.node_id); + const { payoutUrl } = await generatePermit2Signature(account, amountInETH, issue.node_id); comment = `${comment}### [ **${user}: [ CLAIM ${amountInETH} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n`; reward[user] = payoutUrl; } else { @@ -76,8 +124,8 @@ export const incentivizeComments = async () => { export const incentivizeCreatorComment = async () => { const logger = getLogger(); const { - mode: { incentiveMode }, - price: { commentElementPricing, issueCreatorMultiplier }, + mode: { incentiveMode, paymentPermitMaxPrice }, + price: { incentives, issueCreatorMultiplier }, payout: { paymentToken, rpc }, } = getBotConfig(); if (!incentiveMode) { @@ -86,29 +134,66 @@ export const incentivizeCreatorComment = async () => { } const context = getBotContext(); const payload = context.payload as Payload; - const org = payload.organization?.login; const issue = payload.issue; - if (!issue || !org) { - logger.info(`Incomplete payload. issue: ${issue}, org: ${org}`); + if (!issue) { + logger.info(`Incomplete payload. issue: ${issue}`); return; } - const assignees = issue?.assignees ?? []; + + if (issue.state_reason !== StateReason.COMPLETED) { + logger.info("incentivizeCreatorComment: comment incentives skipped because the issue was not closed as completed"); + return; + } + + if (paymentPermitMaxPrice == 0 || !paymentPermitMaxPrice) { + logger.info(`incentivizeCreatorComment: skipping to generate permit2 url, reason: { paymentPermitMaxPrice: ${paymentPermitMaxPrice}}`); + return; + } + + const issueDetailed = bountyInfo(issue); + if (!issueDetailed.isBounty) { + logger.info(`incentivizeCreatorComment: its not a bounty`); + return; + } + + const comments = await getAllIssueComments(issue.number); + const permitComments = comments.filter( + (content) => content.body.includes("Task Creator Reward") && content.body.includes("https://pay.ubq.fi?claim=") && content.user.type == UserType.Bot + ); + if (permitComments.length > 0) { + logger.info(`incentivizeCreatorComment: skip to generate a permit url because it has been already posted`); + return; + } + + const assignees = issue.assignees ?? []; const assignee = assignees.length > 0 ? assignees[0] : undefined; if (!assignee) { - logger.info("Skipping payment permit generation because `assignee` is `undefined`."); + logger.info("incentivizeCreatorComment: skipping payment permit generation because `assignee` is `undefined`."); return; } - const description = await getIssueDescription(issue.number); + const description = await getIssueDescription(issue.number, "html"); + if (!description) { + logger.info(`Skipping to generate a permit url because issue description is empty. description: ${description}`); + return; + } logger.info(`Getting the issue description done. description: ${description}`); const creator = issue.user; - if (creator?.type === UserType.Bot || creator?.login === issue?.assignee) { + if (creator.type === UserType.Bot || creator.login === issue.assignee) { logger.info("Issue creator assigneed himself or Bot created this issue."); return; } const tokenSymbol = await getTokenSymbol(paymentToken, rpc); - const result = await generatePermitForComments(creator?.login, [description], issueCreatorMultiplier, commentElementPricing, tokenSymbol, issue.node_id); + const result = await generatePermitForComments( + creator.login, + [description], + issueCreatorMultiplier, + incentives, + tokenSymbol, + issue.node_id, + paymentPermitMaxPrice + ); if (result.payoutUrl) { logger.info(`Permit url generated for creator. reward: ${result.payoutUrl}`); @@ -123,19 +208,28 @@ const generatePermitForComments = async ( user: string, comments: string[], multiplier: number, - commentElementPricing: Record, + incentives: Incentives, tokenSymbol: string, - node_id: string -): Promise<{ comment: string; payoutUrl?: string; amountInETH?: string }> => { + node_id: string, + paymentPermitMaxPrice: number +): Promise<{ comment: string; payoutUrl?: string; amountInETH?: Decimal }> => { const logger = getLogger(); const commentsByNode = await parseComments(comments, ItemsToExclude); - const rewardValue = calculateRewardValue(commentsByNode, commentElementPricing); + const rewardValue = calculateRewardValue(commentsByNode, incentives); + if (rewardValue.equals(0)) { + logger.info(`No reward for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`); + return { comment: "" }; + } logger.debug(`Comment parsed for the user: ${user}. comments: ${JSON.stringify(commentsByNode)}, sum: ${rewardValue}`); const account = await getWalletAddress(user); - const amountInETH = ((rewardValue * multiplier) / 1000).toString(); - let comment = ""; + const amountInETH = rewardValue.mul(multiplier); + if (amountInETH.gt(paymentPermitMaxPrice)) { + logger.info(`Skipping issue creator reward for user ${user} because reward is higher than payment permit max price`); + return { comment: "" }; + } + let comment = `#### Task Creator Reward\n`; if (account) { - const payoutUrl = await generatePermit2Signature(account, amountInETH, node_id); + const { payoutUrl } = await generatePermit2Signature(account, amountInETH, node_id); comment = `${comment}### [ **${user}: [ CLAIM ${amountInETH} ${tokenSymbol.toUpperCase()} ]** ](${payoutUrl})\n`; return { comment, payoutUrl }; } else { @@ -146,18 +240,29 @@ const generatePermitForComments = async ( * @dev Calculates the reward values for a given comments. We'll improve the formula whenever we get the better one. * * @param comments - The comments to calculate the reward for - * @param commentElementPricing - The basic price table for reward calculation + * @param incentives - The basic price table for reward calculation * @returns - The reward value */ -const calculateRewardValue = (comments: Record, commentElementPricing: CommentElementPricing): number => { - let sum = 0; +const calculateRewardValue = (comments: Record, incentives: Incentives): Decimal => { + let sum = new Decimal(0); for (const key of Object.keys(comments)) { - const rewardValue = commentElementPricing[key]; const value = comments[key]; - if (key == MarkdownItem.Text || key == MarkdownItem.Paragraph) { - sum += value.length * rewardValue; + + // if it's a text node calculate word count and multiply with the reward value + if (key == "#text") { + if (!incentives.comment.totals.word) { + continue; + } + const wordReward = new Decimal(incentives.comment.totals.word); + const reward = wordReward.mul(value.map((str) => str.trim().split(" ").length).reduce((totalWords, wordCount) => totalWords + wordCount, 0)); + sum = sum.add(reward); } else { - sum += rewardValue; + if (!incentives.comment.elements[key]) { + continue; + } + const rewardValue = new Decimal(incentives.comment.elements[key]); + const reward = rewardValue.mul(value.length); + sum = sum.add(reward); } } diff --git a/src/handlers/pricing/action.ts b/src/handlers/pricing/action.ts index e32b899bc..cd9dfa8a4 100644 --- a/src/handlers/pricing/action.ts +++ b/src/handlers/pricing/action.ts @@ -1,5 +1,6 @@ import { getBotConfig, getBotContext, getLogger } from "../../bindings"; -import { addLabelToIssue, clearAllPriceLabelsOnIssue, createLabel, getLabel } from "../../helpers"; +import { GLOBAL_STRINGS } from "../../configs"; +import { addCommentToIssue, addLabelToIssue, clearAllPriceLabelsOnIssue, createLabel, getLabel, calculateWeight } from "../../helpers"; import { Payload } from "../../types"; import { handleLabelsAccess } from "../access"; import { getTargetPriceLabel } from "../shared"; @@ -12,17 +13,28 @@ export const pricingLabelLogic = async (): Promise => { if (!payload.issue) return; const labels = payload.issue.labels; + logger.info(`Checking if the issue is a parent issue.`); + if (payload.issue.body && isParentIssue(payload.issue.body)) { + logger.error("Identified as parent issue. Disabling price label."); + const issuePrices = labels.filter((label) => label.name.toString().startsWith("Price:")); + if (issuePrices.length) { + await addCommentToIssue(GLOBAL_STRINGS.skipPriceLabelGenerationComment, payload.issue.number); + await clearAllPriceLabelsOnIssue(); + } + return; + } const valid = await handleLabelsAccess(); if (!valid) { return; } + const { assistivePricing } = config.mode; 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)); - const minTimeLabel = timeLabels.length > 0 ? timeLabels.reduce((a, b) => (a.weight < b.weight ? a : b)).name : undefined; - const minPriorityLabel = priorityLabels.length > 0 ? priorityLabels.reduce((a, b) => (a.weight < b.weight ? a : b)).name : undefined; + const minTimeLabel = timeLabels.length > 0 ? timeLabels.reduce((a, b) => (calculateWeight(a) < calculateWeight(b) ? a : b)).name : undefined; + const minPriorityLabel = priorityLabels.length > 0 ? priorityLabels.reduce((a, b) => (calculateWeight(a) < calculateWeight(b) ? a : b)).name : undefined; const targetPriceLabel = getTargetPriceLabel(minTimeLabel, minPriorityLabel); if (targetPriceLabel) { @@ -33,7 +45,8 @@ export const pricingLabelLogic = async (): Promise => { await clearAllPriceLabelsOnIssue(); const exist = await getLabel(targetPriceLabel); - if (!exist) { + + if (assistivePricing && !exist) { logger.info(`${targetPriceLabel} doesn't exist on the repo, creating...`); await createLabel(targetPriceLabel, "price"); } @@ -44,3 +57,8 @@ export const pricingLabelLogic = async (): Promise => { logger.info(`Skipping action...`); } }; + +export const isParentIssue = (body: string) => { + const parentPattern = /-\s+\[( |x)\]\s+#\d+/; + return body.match(parentPattern); +}; diff --git a/src/handlers/pricing/pre.ts b/src/handlers/pricing/pre.ts index 29703bda7..4d048d1ca 100644 --- a/src/handlers/pricing/pre.ts +++ b/src/handlers/pricing/pre.ts @@ -1,5 +1,5 @@ import { getBotConfig, getLogger } from "../../bindings"; -import { createLabel, listLabelsForRepo } from "../../helpers"; +import { calculateWeight, createLabel, listLabelsForRepo } from "../../helpers"; import { calculateBountyPrice } from "../shared"; /** @@ -10,12 +10,19 @@ export const validatePriceLabels = async (): Promise => { const config = getBotConfig(); const logger = getLogger(); + const { assistivePricing } = config.mode; + + if (!assistivePricing) { + logger.info(`Assistive Pricing is disabled`); + return; + } + const timeLabels = config.price.timeLabels.map((i) => i.name); const priorityLabels = config.price.priorityLabels.map((i) => i.name); const aiLabels: string[] = []; for (const timeLabel of config.price.timeLabels) { for (const priorityLabel of config.price.priorityLabels) { - const targetPrice = calculateBountyPrice(timeLabel.weight, priorityLabel.weight, config.price.baseMultiplier); + const targetPrice = calculateBountyPrice(calculateWeight(timeLabel), calculateWeight(priorityLabel), config.price.baseMultiplier); const targetPriceLabel = `Price: ${targetPrice} USD`; aiLabels.push(targetPriceLabel); } diff --git a/src/handlers/processors.ts b/src/handlers/processors.ts index d64e07eaa..a98c65611 100644 --- a/src/handlers/processors.ts +++ b/src/handlers/processors.ts @@ -3,16 +3,17 @@ import { closePullRequestForAnIssue, commentWithAssignMessage } from "./assign"; import { pricingLabelLogic, validatePriceLabels } from "./pricing"; import { checkBountiesToUnassign, collectAnalytics, checkWeeklyUpdate } from "./wildcard"; import { nullHandler } from "./shared"; -import { handleComment, issueClosedCallback, issueReopenedCallback } from "./comment"; +import { handleComment, issueClosedCallback, issueCreatedCallback, issueReopenedCallback } from "./comment"; import { checkPullRequests } from "./assign/auto"; import { createDevPoolPR } from "./pull-request"; import { runOnPush } from "./push"; import { incentivizeComments, incentivizeCreatorComment } from "./payout"; +import { findDuplicateOne } from "./issue"; export const processors: Record = { [GithubEvent.ISSUES_OPENED]: { pre: [nullHandler], - action: [nullHandler], // SHOULD not set `issueCreatedCallback` until the exploit issue resolved. https://github.com/ubiquity/ubiquibot/issues/535 + action: [findDuplicateOne, issueCreatedCallback], post: [nullHandler], }, [GithubEvent.ISSUES_REOPENED]: { diff --git a/src/handlers/shared/pricing.ts b/src/handlers/shared/pricing.ts index 65c1079d0..0a543bc7c 100644 --- a/src/handlers/shared/pricing.ts +++ b/src/handlers/shared/pricing.ts @@ -1,4 +1,5 @@ import { getBotConfig } from "../../bindings"; +import { calculateWeight } from "../../helpers"; export const calculateBountyPrice = (timeValue: number, priorityValue: number, baseValue?: number): number => { const botConfig = getBotConfig(); @@ -12,8 +13,8 @@ export const getTargetPriceLabel = (timeLabel: string | undefined, priorityLabel const botConfig = getBotConfig(); let targetPriceLabel: string | undefined = undefined; if (timeLabel && priorityLabel) { - const timeWeight = botConfig.price.timeLabels.find((item) => item.name === timeLabel)?.weight; - const priorityWeight = botConfig.price.priorityLabels.find((item) => item.name === priorityLabel)?.weight; + const timeWeight = calculateWeight(botConfig.price.timeLabels.find((item) => item.name === timeLabel)); + const priorityWeight = calculateWeight(botConfig.price.priorityLabels.find((item) => item.name === priorityLabel)); if (timeWeight && priorityWeight) { const bountyPrice = calculateBountyPrice(timeWeight, priorityWeight); targetPriceLabel = `Price: ${bountyPrice} USD`; diff --git a/src/handlers/wildcard/analytics.ts b/src/handlers/wildcard/analytics.ts index 40e861ac7..62f24edf9 100644 --- a/src/handlers/wildcard/analytics.ts +++ b/src/handlers/wildcard/analytics.ts @@ -1,6 +1,6 @@ import { getMaxIssueNumber, upsertIssue, upsertUser } from "../../adapters/supabase"; import { getBotConfig, getLogger } from "../../bindings"; -import { listIssuesForRepo, getUser } from "../../helpers"; +import { listIssuesForRepo, getUser, calculateWeight } from "../../helpers"; import { Issue, IssueType, User, UserProfile } from "../../types"; import { getTargetPriceLabel } from "../shared"; @@ -24,8 +24,8 @@ export const bountyInfo = ( const isBounty = timeLabels.length > 0 && priorityLabels.length > 0; - const minTimeLabel = timeLabels.length > 0 ? timeLabels.reduce((a, b) => (a.weight < b.weight ? a : b)).name : undefined; - const minPriorityLabel = priorityLabels.length > 0 ? priorityLabels.reduce((a, b) => (a.weight < b.weight ? a : b)).name : undefined; + const minTimeLabel = timeLabels.length > 0 ? timeLabels.reduce((a, b) => (calculateWeight(a) < calculateWeight(b) ? a : b)).name : undefined; + const minPriorityLabel = priorityLabels.length > 0 ? priorityLabels.reduce((a, b) => (calculateWeight(a) < calculateWeight(b) ? a : b)).name : undefined; const priceLabel = getTargetPriceLabel(minTimeLabel, minPriorityLabel); @@ -83,26 +83,28 @@ export const collectAnalytics = async (): Promise => { .toString()}` ); - await Promise.all(userProfilesToUpsert.map(async (i) => upsertUser(i))); + await Promise.all(userProfilesToUpsert.map((i) => upsertUser(i))); // No need to update the record for the bounties already closed const bountiesToUpsert = bounties.filter((bounty) => (bounty.state === IssueType.CLOSED ? bounty.number > maximumIssueNumber : true)); logger.info(`Upserting bounties: ${bountiesToUpsert.map((i) => i.title).toString()}`); await Promise.all( - bountiesToUpsert.map(async (i) => { + bountiesToUpsert.map((i) => { const additions = bountyInfo(i as Issue); if (additions.timelabel && additions.priorityLabel && additions.priceLabel) - await upsertIssue(i as Issue, { + return upsertIssue(i as Issue, { labels: { timeline: additions.timelabel, priority: additions.priorityLabel, price: additions.priceLabel, }, }); + return undefined; }) ); if (issues.length < perPage) fetchDone = true; else curPage++; } + logger.info("Collecting analytics finished..."); }; diff --git a/src/handlers/wildcard/unassign.ts b/src/handlers/wildcard/unassign.ts index b94b2c1c7..8e06ecebd 100644 --- a/src/handlers/wildcard/unassign.ts +++ b/src/handlers/wildcard/unassign.ts @@ -72,7 +72,7 @@ const checkBountyToUnassign = async (issue: Issue): Promise => { ); } else await addCommentToIssue( - `${askUpdate} @${assignees[0]}? If you would like to release the bounty back to the DevPool, please comment \`/unassign\` \nLast activity time: ${lastActivity}`, + `${askUpdate} @${assignees[0]}? If you would like to release the bounty back to the DevPool, please comment \`/stop\` \nLast activity time: ${lastActivity}`, issue.number ); } diff --git a/src/handlers/wildcard/weekly.ts b/src/handlers/wildcard/weekly.ts index 3f52bcd5b..8d9dbdc70 100644 --- a/src/handlers/wildcard/weekly.ts +++ b/src/handlers/wildcard/weekly.ts @@ -2,7 +2,7 @@ import { run } from "./weekly/action"; import { getLastWeeklyTime, updateLastWeeklyTime } from "../../adapters/supabase"; import { getBotConfig, getBotContext } from "../../bindings"; -const SEVEN_DAYS = 604800; // 7 days in seconds +const SEVEN_DAYS = 7 * 24 * 60 * 60 * 1000; // 7 days in milliseconds export const checkWeeklyUpdate = async () => { const { log } = getBotContext(); @@ -13,10 +13,12 @@ export const checkWeeklyUpdate = async () => { log.info(`Skipping to collect the weekly analytics, reason: mode=${disableAnalytics}`); return; } - const curTime = Date.now() / 1000; + const curTime = new Date(); const lastTime = await getLastWeeklyTime(); - if (lastTime + SEVEN_DAYS < curTime) { + if (lastTime == undefined || new Date(lastTime.getTime() + SEVEN_DAYS) < curTime) { await run(); await updateLastWeeklyTime(curTime); + } else { + log.info(`Skipping to collect the weekly analytics because 7 days have not passed`); } }; diff --git a/src/handlers/wildcard/weekly/action.ts b/src/handlers/wildcard/weekly/action.ts index 4a26ca9fe..09e06e878 100644 --- a/src/handlers/wildcard/weekly/action.ts +++ b/src/handlers/wildcard/weekly/action.ts @@ -6,7 +6,7 @@ import path from "path"; import axios from "axios"; import Jimp from "jimp"; import nodeHtmlToImage from "node-html-to-image"; -import { getBotContext } from "../../../bindings"; +import { getBotConfig, getBotContext } from "../../../bindings"; import { telegramPhotoNotifier } from "../../../adapters"; import { Context } from "probot"; import { Payload } from "../../../types"; @@ -33,31 +33,41 @@ const fetchEvents = async (context: Context): Promise => { const perPage = 30; while (shouldFetch) { try { + let events; if (payload.organization) { - const { data: pubOrgEvents, headers } = await context.octokit.activity.listPublicOrgEvents({ + events = await context.octokit.activity.listPublicOrgEvents({ org: payload.organization.login, per_page: perPage, page: currentPage, }); + } else { + events = await context.octokit.activity.listRepoEvents({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + per_page: perPage, + page: currentPage, + }); + } + const pubEvents = events.data; + const headers = events.headers; - await checkRateLimitGit(headers); + await checkRateLimitGit(headers); - pubOrgEvents.forEach((elem: any) => { - const elemTimestamp = new Date(elem.created_at as string).getTime(); - if (elemTimestamp <= startTimestamp && elemTimestamp >= endTimestamp) { - //pass - elemList.push(elem); - } else if (elemTimestamp > startTimestamp) { - //outta range - //skip - } else { - //fail end - shouldFetch = false; - } - }); + pubEvents.forEach((elem: any) => { + const elemTimestamp = new Date(elem.created_at as string).getTime(); + if (elemTimestamp <= startTimestamp && elemTimestamp >= endTimestamp) { + //pass + elemList.push(elem); + } else if (elemTimestamp > startTimestamp) { + //outta range + //skip + } else { + //fail end + shouldFetch = false; + } + }); - currentPage++; - } + currentPage++; } catch (error) { shouldFetch = false; } @@ -339,5 +349,12 @@ export const run = async () => { const dataPadded = await fetchSummary(repository); await htmlImage(summaryInfo); await compositeImage(); - await processTelegram(dataPadded); + + const { telegram } = getBotConfig(); + if (telegram.token) { + await processTelegram(dataPadded); + } else { + const log = context.log; + log.info("Skipping processTelegram because no token was set."); + } }; diff --git a/src/helpers/comment.ts b/src/helpers/comment.ts index 825958cc9..cba6c165f 100644 --- a/src/helpers/comment.ts +++ b/src/helpers/comment.ts @@ -1,44 +1,41 @@ -type MdastNode = { - type: string; - value: string; - children: MdastNode[]; +import * as parse5 from "parse5"; + +type Node = { + nodeName: string; + tagName?: string; + value?: string; + childNodes?: Node[]; }; -const cachedResult: Record = {}; -const traverse = (node: MdastNode, itemsToExclude: string[]): Record => { - if (!cachedResult[node.type]) { - cachedResult[node.type] = []; + +const traverse = (result: Record, node: Node, itemsToExclude: string[]): Record => { + if (itemsToExclude.includes(node.nodeName)) { + return result; } - if (!itemsToExclude.includes(node.type)) { - // skip pushing if the node type has been excluded - cachedResult[node.type].push(node.value); - } else if (node.children.length > 0) { - node.children.forEach((child) => traverse(child, itemsToExclude)); + if (!result[node.nodeName]) { + result[node.nodeName] = []; } - return cachedResult; -}; + result[node.nodeName].push(node.value?.trim() ?? ""); + + if (node.childNodes && node.childNodes.length > 0) { + node.childNodes.forEach((child) => traverse(result, child, itemsToExclude)); + } -export const parseComments = async (comments: string[], itemsToExclude: string[]): Promise> => { - const { fromMarkdown } = await import("mdast-util-from-markdown"); - const { gfmFromMarkdown } = await import("mdast-util-gfm"); - const { gfm } = await import("micromark-extension-gfm"); + return result; +}; +export const parseComments = (comments: string[], itemsToExclude: string[]): Record => { const result: Record = {}; + for (const comment of comments) { - const tree = fromMarkdown(comment, { - extensions: [gfm()], - mdastExtensions: [gfmFromMarkdown()], - }); - - const parsedContent = traverse(tree as MdastNode, itemsToExclude); - for (const key of Object.keys(parsedContent)) { - if (Object.keys(result).includes(key)) { - result[key].push(...parsedContent[key]); - } else { - result[key] = parsedContent[key]; - } - } + const fragment = parse5.parseFragment(comment); + traverse(result, fragment as Node, itemsToExclude); + } + + // remove empty values + if (result["#text"]) { + result["#text"] = result["#text"].filter((str) => str.length > 0); } return result; diff --git a/src/helpers/index.ts b/src/helpers/index.ts index 1d447003d..ad8ee0cb9 100644 --- a/src/helpers/index.ts +++ b/src/helpers/index.ts @@ -8,3 +8,4 @@ export * from "./contracts"; export * from "./comment"; export * from "./payout"; export * from "./file"; +export * from "./similarity"; diff --git a/src/helpers/issue.ts b/src/helpers/issue.ts index cb07df7ee..00ccd0e93 100644 --- a/src/helpers/issue.ts +++ b/src/helpers/issue.ts @@ -1,13 +1,13 @@ import { Context } from "probot"; -import { getBotContext, getLogger } from "../bindings"; +import { getBotContext, getLogger, loadConfig } from "../bindings"; import { AssignEvent, Comment, IssueType, Payload } from "../types"; import { checkRateLimitGit } from "../utils"; -import { DEFAULT_TIME_RANGE_FOR_MAX_ISSUE, DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED } from "../configs"; export const clearAllPriceLabelsOnIssue = async (): Promise => { const context = getBotContext(); const logger = getLogger(); const payload = context.payload as Payload; + if (!payload.issue) return; const labels = payload.issue.labels; @@ -48,7 +48,13 @@ export const addLabelToIssue = async (labelName: string) => { } }; -export const listIssuesForRepo = async (state: "open" | "closed" | "all" = "open", per_page = 30, page = 1) => { +export const listIssuesForRepo = async ( + state: "open" | "closed" | "all" = "open", + per_page = 30, + page = 1, + sort: "created" | "updated" | "comments" = "created", + direction: "desc" | "asc" = "desc" +) => { const context = getBotContext(); const payload = context.payload as Payload; @@ -58,6 +64,8 @@ export const listIssuesForRepo = async (state: "open" | "closed" | "all" = "open state, per_page, page, + sort, + direction, }); await checkRateLimitGit(response.headers); @@ -175,7 +183,7 @@ export const getCommentsOfIssue = async (issue_number: number): Promise => { +export const getIssueDescription = async (issue_number: number, format: "raw" | "html" | "text" = "raw"): Promise => { const context = getBotContext(); const logger = getLogger(); const payload = context.payload as Payload; @@ -186,17 +194,30 @@ export const getIssueDescription = async (issue_number: number): Promise owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: issue_number, + mediaType: { + format, + }, }); await checkRateLimitGit(response?.headers); - if (response.data.body) result = response.data.body; + switch (format) { + case "raw": + result = response.data.body ?? ""; + break; + case "html": + result = response.data.body_html ?? ""; + break; + case "text": + result = response.data.body_text ?? ""; + break; + } } catch (e: unknown) { logger.debug(`Getting issue description failed!, reason: ${e}`); } return result; }; -export const getAllIssueComments = async (issue_number: number): Promise => { +export const getAllIssueComments = async (issue_number: number, format: "raw" | "html" | "text" | "full" = "raw"): Promise => { const context = getBotContext(); const payload = context.payload as Payload; @@ -211,6 +232,9 @@ export const getAllIssueComments = async (issue_number: number): Promise => { + const permissionForRepo = await checkUserPermissionForRepo(username, context); + const permissionForOrg = await checkUserPermissionForOrg(username, context); + const userPermission = await getUserPermission(username, context); + + return permissionForOrg || permissionForRepo || userPermission === "admin" || userPermission === "billing_manager"; +}; + +export const checkUserPermissionForRepo = async (username: string, context: Context): Promise => { + const logger = getLogger(); + const payload = context.payload as Payload; + + try { + const res = await context.octokit.rest.repos.checkCollaborator({ + owner: payload.repository.owner.login, + repo: payload.repository.name, + username, + }); + + return res.status === 204; + } catch (e: unknown) { + logger.error(`Checking if user permisson for repo failed!, reason: ${e}`); + return false; + } +}; + +export const checkUserPermissionForOrg = async (username: string, context: Context): Promise => { + const logger = getLogger(); + const payload = context.payload as Payload; + if (!payload.organization) return false; + + try { + await context.octokit.rest.orgs.checkMembershipForUser({ + org: payload.organization.login, + username, + }); + // skipping status check due to type error of checkMembershipForUser function of octokit + return true; + } catch (e: unknown) { + logger.error(`Checking if user permisson for org failed!, reason: ${e}`); + return false; + } +}; + export const getUserPermission = async (username: string, context: Context): Promise => { const logger = getLogger(); const payload = context.payload as Payload; @@ -396,8 +464,24 @@ export const removeLabel = async (name: string) => { } }; +export const getAllPullRequests = async (context: Context, state: "open" | "closed" | "all" = "open") => { + const prArr = []; + let fetchDone = false; + const perPage = 100; + let curPage = 1; + while (!fetchDone) { + const prs = await getPullRequests(context, state, perPage, curPage); + + // push the objects to array + prArr.push(...prs); + + if (prs.length < perPage) fetchDone = true; + else curPage++; + } + return prArr; +}; // Use `context.octokit.rest` to get the pull requests for the repository -export const getPullRequests = async (context: Context, state: "open" | "closed" | "all" = "open") => { +export const getPullRequests = async (context: Context, state: "open" | "closed" | "all" = "open", per_page: number, page: number) => { const logger = getLogger(); const payload = context.payload as Payload; try { @@ -405,6 +489,8 @@ export const getPullRequests = async (context: Context, state: "open" | "closed" owner: payload.repository.owner.login, repo: payload.repository.name, state, + per_page, + page, }); return pulls; } catch (e: unknown) { @@ -532,7 +618,7 @@ export const getOpenedPullRequestsForAnIssue = async (issueNumber: number, userN export const getOpenedPullRequests = async (username: string) => { const context = getBotContext(); - const prs = await getPullRequests(context, "open"); + const prs = await getAllPullRequests(context, "open"); return prs.filter((pr) => !pr.draft && (pr.user?.login === username || !username)); }; @@ -554,8 +640,10 @@ export const getCommitsOnPullRequest = async (pullNumber: number) => { }; export const getAvailableOpenedPullRequests = async (username: string) => { - if (!DEFAULT_TIME_RANGE_FOR_MAX_ISSUE_ENABLED) return []; const context = getBotContext(); + const botConfig = await loadConfig(context); + if (!botConfig.unassign.timeRangeForMaxIssueEnabled) return []; + const opened_prs = await getOpenedPullRequests(username); const result = []; @@ -569,7 +657,7 @@ export const getAvailableOpenedPullRequests = async (username: string) => { 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) { + if (reviews.length === 0 && (new Date().getTime() - new Date(pr.created_at).getTime()) / (1000 * 60 * 60) >= botConfig.unassign.timeRangeForMaxIssue) { result.push(pr); } } diff --git a/src/helpers/label.ts b/src/helpers/label.ts index 0f30bb265..a02d6c5eb 100644 --- a/src/helpers/label.ts +++ b/src/helpers/label.ts @@ -4,6 +4,7 @@ import { COLORS } from "../configs"; import { calculateBountyPrice } from "../handlers"; import { Label, Payload } from "../types"; import { deleteLabel } from "./issue"; +import { calculateWeight } from "../helpers"; export const listLabelsForRepo = async (per_page?: number, page?: number): Promise => { const context = getBotContext(); @@ -67,11 +68,11 @@ export const updateLabelsFromBaseRate = async (owner: string, repo: string, cont for (const timeLabel of config.price.timeLabels) { for (const priorityLabel of config.price.priorityLabels) { - const targetPrice = calculateBountyPrice(timeLabel.weight, priorityLabel.weight, config.price.baseMultiplier); + const targetPrice = calculateBountyPrice(calculateWeight(timeLabel), calculateWeight(priorityLabel), config.price.baseMultiplier); const targetPriceLabel = `Price: ${targetPrice} USD`; newLabels.push(targetPriceLabel); - const previousTargetPrice = calculateBountyPrice(timeLabel.weight, priorityLabel.weight, previousBaseRate); + const previousTargetPrice = calculateBountyPrice(calculateWeight(timeLabel), calculateWeight(priorityLabel), previousBaseRate); const previousTargetPriceLabel = `Price: ${previousTargetPrice} USD`; previousLabels.push(previousTargetPriceLabel); } diff --git a/src/helpers/permit.ts b/src/helpers/permit.ts index de17b98eb..ba1bcaaa0 100644 --- a/src/helpers/permit.ts +++ b/src/helpers/permit.ts @@ -1,10 +1,49 @@ import { MaxUint256, PermitTransferFrom, SignatureTransfer } from "@uniswap/permit2-sdk"; import { BigNumber, ethers } from "ethers"; -import { getBotConfig, getLogger } from "../bindings"; +import { getBotConfig, getBotContext, getLogger } from "../bindings"; import { keccak256, toUtf8Bytes } from "ethers/lib/utils"; +import Decimal from "decimal.js"; +import { Payload } from "../types"; +import { savePermit } from "../adapters/supabase"; const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // same on all networks +export type Permit = { + id: number; + createdAt: Date; + organizationId: number | null; + repositoryId: number; + issueId: number; + networkId: number; + bountyHunterId: number; + bountyHunterAddress: string; + tokenAddress: string; + payoutAmount: string; + nonce: string; + deadline: string; + signature: string; + walletOwnerAddress: string; +}; + +export type InsertPermit = Omit; + +type TxData = { + permit: { + permitted: { + token: string; + amount: string; + }; + nonce: string; + deadline: string; + }; + transferDetails: { + to: string; + requestedAmount: string; + }; + owner: string; + signature: string; +}; + /** * Generates permit2 signature data with `spender` and `amountInETH` * @@ -13,7 +52,7 @@ const PERMIT2_ADDRESS = "0x000000000022D473030F116dDEE9F6B43aC78BA3"; // same on * * @returns Permit2 url including base64 encocded data */ -export const generatePermit2Signature = async (spender: string, amountInEth: string, identifier: string): Promise => { +export const generatePermit2Signature = async (spender: string, amountInEth: Decimal, identifier: string): Promise<{ txData: TxData; payoutUrl: string }> => { const { payout: { networkId, privateKey, permitBaseUrl, rpc, paymentToken }, } = getBotConfig(); @@ -26,7 +65,7 @@ export const generatePermit2Signature = async (spender: string, amountInEth: str // token we are permitting to be transferred token: paymentToken, // amount we are permitting to be transferred - amount: ethers.utils.parseUnits(amountInEth, 18), + amount: ethers.utils.parseUnits(amountInEth.toString(), 18), }, // who can transfer the tokens spender: spender, @@ -38,7 +77,7 @@ export const generatePermit2Signature = async (spender: string, amountInEth: str const { domain, types, values } = SignatureTransfer.getPermitData(permitTransferFromData, PERMIT2_ADDRESS, networkId); const signature = await adminWallet._signTypedData(domain, types, values); - const txData = { + const txData: TxData = { permit: { permitted: { token: permitTransferFromData.permitted.token, @@ -57,7 +96,43 @@ export const generatePermit2Signature = async (spender: string, amountInEth: str const base64encodedTxData = Buffer.from(JSON.stringify(txData)).toString("base64"); - const result = `${permitBaseUrl}?claim=${base64encodedTxData}&network=${networkId}`; - logger.info(`Generated permit2 url: ${result}`); - return result; + const payoutUrl = `${permitBaseUrl}?claim=${base64encodedTxData}&network=${networkId}`; + logger.info(`Generated permit2 url: ${payoutUrl}`); + return { txData, payoutUrl }; +}; + +export const savePermitToDB = async (bountyHunterId: number, txData: TxData): Promise => { + const logger = getLogger(); + + const context = getBotContext(); + const payload = context.payload as Payload; + const issue = payload.issue; + const repository = payload.repository; + const organization = payload.organization; + if (!issue || !repository) { + logger.error("Cannot save permit to DB, missing issue, repository or organization"); + throw new Error("Cannot save permit to DB, missing issue, repository or organization"); + } + + const { payout } = getBotConfig(); + const { networkId } = payout; + + const permit: InsertPermit = { + organizationId: organization?.id ?? null, + repositoryId: repository?.id, + issueId: issue?.id, + networkId: networkId, + bountyHunterId: bountyHunterId, + tokenAddress: txData.permit.permitted.token, + payoutAmount: txData.permit.permitted.amount, + bountyHunterAddress: txData.transferDetails.to, + nonce: txData.permit.nonce, + deadline: txData.permit.deadline, + signature: txData.signature, + walletOwnerAddress: txData.owner, + }; + + const savedPermit = await savePermit(permit); + logger.info(`Saved permit to DB: ${JSON.stringify(savedPermit)}`); + return savedPermit; }; diff --git a/src/helpers/shared.ts b/src/helpers/shared.ts index a1a0d4c6a..646e0d8e9 100644 --- a/src/helpers/shared.ts +++ b/src/helpers/shared.ts @@ -1,5 +1,5 @@ import { getBotContext } from "../bindings"; -import { Payload, UserType } from "../types"; +import { LabelItem, Payload, UserType } from "../types"; const contextNamesToSkip = ["workflow_run"]; @@ -19,3 +19,27 @@ export const shouldSkip = (): { skip: boolean; reason: string } => { }; export const wait = (ms: number) => new Promise((r) => setTimeout(r, ms)); + +export const calculateWeight = (label: LabelItem | undefined): number => { + if (!label) return 0; + const matches = label.name.match(/\d+/); + const number = matches && matches.length > 0 ? parseInt(matches[0]) || 0 : 0; + if (label.name.toLowerCase().includes("priority")) return number; + if (label.name.toLowerCase().includes("hour")) return number * 0.125; + if (label.name.toLowerCase().includes("day")) return 1 + (number - 1) * 0.25; + if (label.name.toLowerCase().includes("week")) return number + 1; + if (label.name.toLowerCase().includes("month")) return 5 + (number - 1) * 8; + return 0; +}; + +export const calculateDuration = (label: LabelItem): number => { + if (!label) return 0; + const matches = label.name.match(/\d+/); + if (label.name.toLowerCase().includes("priority")) return 0; + const number = matches && matches.length > 0 ? parseInt(matches[0]) || 0 : 0; + if (label.name.toLowerCase().includes("hour")) return number * 3600; + if (label.name.toLowerCase().includes("day")) return number * 86400; + if (label.name.toLowerCase().includes("week")) return number * 604800; + if (label.name.toLowerCase().includes("month")) return number * 2592000; + return 0; +}; diff --git a/src/helpers/similarity.ts b/src/helpers/similarity.ts new file mode 100644 index 000000000..a5743e562 --- /dev/null +++ b/src/helpers/similarity.ts @@ -0,0 +1,104 @@ +import { getLogger } from "../bindings"; +import axios, { AxiosError } from "axios"; +import { ajv } from "../utils"; +import { Static, Type } from "@sinclair/typebox"; +import { backOff } from "exponential-backoff"; +import { Issue } from "../types"; + +export const extractImportantWords = async (issue: Issue): Promise => { + const res = await getAnswerFromChatGPT( + "", + `${ + process.env.CHATGPT_USER_PROMPT_FOR_IMPORTANT_WORDS || + "I need your help to find important words (e.g. unique adjectives) from github issue below and I want to parse them easily so please separate them using #(No other contexts needed). Please separate the words by # so I can parse them easily. Please answer simply as I only need the important words. Here is the issue content.\n" + } '${`Issue title: ${issue.title}\nIssue content: ${issue.body}`}'`, + parseFloat(process.env.IMPORTANT_WORDS_AI_TEMPERATURE || "0") + ); + if (res === "") return []; + return res.split(/[,# ]/); +}; + +export const measureSimilarity = async (first: Issue, second: Issue): Promise => { + const res = await getAnswerFromChatGPT( + "", + `${( + process.env.CHATGPT_USER_PROMPT_FOR_MEASURE_SIMILARITY || + 'I have two github issues and I need to measure the possibility of the 2 issues are the same content (I need to parse the % so other contents are not needed and give me only the number in %).\n Give me in number format and add % after the number.\nDo not tell other things since I only need the number (e.g. 85%). Here are two issues:\n 1. "%first%"\n2. "%second%"' + ) + .replace("%first%", `Issue title: ${first.title}\nIssue content: ${first.body}`) + .replace("%second%", `Issue title: ${second.title}\nIssue content: ${second.body}`)}`, + parseFloat(process.env.MEASURE_SIMILARITY_AI_TEMPERATURE || "0") + ); + const matches = res.match(/\d+/); + const percent = matches && matches.length > 0 ? parseInt(matches[0]) || 0 : 0; + return percent; +}; + +const ChatMessageSchema = Type.Object({ + content: Type.String(), +}); + +const ChoiceSchema = Type.Object({ + message: ChatMessageSchema, +}); + +const ChoicesSchema = Type.Object({ + choices: Type.Array(ChoiceSchema), +}); + +type Choices = Static; + +export const getAnswerFromChatGPT = async (systemPrompt: string, userPrompt: string, temperature = 0, max_tokens = 1500): Promise => { + const logger = getLogger(); + const body = JSON.stringify({ + model: "gpt-3.5-turbo", + messages: [ + { + role: "system", + content: systemPrompt, + }, + { + role: "user", + content: userPrompt, + }, + ], + max_tokens, + temperature, + stream: false, + }); + const config = { + method: "post", + url: `${process.env.OPENAI_API_HOST || "https://api.openai.com"}/v1/chat/completions`, + headers: { + Authorization: `Bearer ${process.env.OPENAI_API_KEY}`, + "Content-Type": "application/json", + }, + data: body, + }; + try { + const response = await backOff(() => axios(config), { + startingDelay: 6000, + retry: (e: AxiosError) => { + if (e.response && e.response.status === 429) return true; + return false; + }, + }); + const data: Choices = response.data; + const validate = ajv.compile(ChoicesSchema); + const valid = validate(data); + if (!valid) { + logger.error(`Error occured from OpenAI`); + return ""; + } + const { choices: choice } = data; + if (choice.length <= 0) { + logger.error(`No result from OpenAI`); + return ""; + } + const answer = choice[0].message.content; + return answer; + } catch (error) { + logger.error(`Getting response from ChatGPT failed: ${error}`); + return ""; + } +}; diff --git a/src/types/config.ts b/src/types/config.ts index 855ba2049..cda063811 100644 --- a/src/types/config.ts +++ b/src/types/config.ts @@ -1,21 +1,36 @@ import { Static, Type } from "@sinclair/typebox"; +import { Level } from "../adapters/supabase"; const LabelItemSchema = Type.Object({ name: Type.String(), - weight: Type.Number(), - value: Type.Optional(Type.Number()), }); export type LabelItem = Static; -const CommentElementPricingSchema = Type.Record(Type.String(), Type.Number()); -export type CommentElementPricing = Static; +const CommentIncentivesSchema = Type.Object({ + elements: Type.Record(Type.String(), Type.Number()), + totals: Type.Object({ + word: Type.Number(), + }), +}); +export type CommentIncentives = Static; + +const IncentivesSchema = Type.Object({ + comment: CommentIncentivesSchema, +}); +export type Incentives = Static; + +const CommandItemSchema = Type.Object({ + name: Type.String(), + enabled: Type.Boolean(), +}); +export type CommandItem = Static; export const PriceConfigSchema = Type.Object({ baseMultiplier: Type.Number(), issueCreatorMultiplier: Type.Number(), timeLabels: Type.Array(LabelItemSchema), priorityLabels: Type.Array(LabelItemSchema), - commentElementPricing: CommentElementPricingSchema, + incentives: IncentivesSchema, defaultLabels: Type.Array(Type.String()), }); export type PriceConfig = Static; @@ -41,12 +56,15 @@ export const PayoutConfigSchema = Type.Object({ export const UnassignConfigSchema = Type.Object({ followUpTime: Type.Number(), disqualifyTime: Type.Number(), + timeRangeForMaxIssue: Type.Number(), + timeRangeForMaxIssueEnabled: Type.Boolean(), }); export const ModeSchema = Type.Object({ - autoPayMode: Type.Boolean(), + paymentPermitMaxPrice: Type.Number(), disableAnalytics: Type.Boolean(), incentiveMode: Type.Boolean(), + assistivePricing: Type.Boolean(), }); export const AssignSchema = Type.Object({ @@ -54,8 +72,9 @@ export const AssignSchema = Type.Object({ }); export const LogConfigSchema = Type.Object({ - level: Type.String(), - ingestionKey: Type.String(), + logEnvironment: Type.String(), + level: Type.Enum(Level), + retryLimit: Type.Number(), }); export const SodiumSchema = Type.Object({ @@ -67,6 +86,9 @@ export const CommentsSchema = Type.Object({ promotionComment: Type.String(), }); +export const CommandConfigSchema = Type.Array(CommandItemSchema); + +export type CommandConfig = Static; export const WalletSchema = Type.Object({ registerWalletWithVerification: Type.Boolean(), }); @@ -82,6 +104,7 @@ export const BotConfigSchema = Type.Object({ assign: AssignSchema, sodium: SodiumSchema, comments: CommentsSchema, + command: CommandConfigSchema, wallet: WalletSchema, }); diff --git a/src/types/payload.ts b/src/types/payload.ts index aa80cf454..527363b6d 100644 --- a/src/types/payload.ts +++ b/src/types/payload.ts @@ -97,6 +97,7 @@ const IssueSchema = Type.Object({ events_url: Type.String(), html_url: Type.String(), id: Type.Number(), + body: Type.Any(), node_id: Type.String(), number: Type.Number(), title: Type.String(), @@ -235,6 +236,8 @@ export const CommentSchema = Type.Object({ updated_at: Type.String({ format: "date-time" }), author_association: Type.String(), body: Type.String(), + body_html: Type.Optional(Type.String()), + body_text: Type.Optional(Type.String()), }); export type Comment = Static; diff --git a/src/utils/helpers.ts b/src/utils/helpers.ts index 415305870..e3965208b 100644 --- a/src/utils/helpers.ts +++ b/src/utils/helpers.ts @@ -1,5 +1,6 @@ -import { CommentElementPricing } from "../types"; -import { WideLabel, WideOrgConfig, WideRepoConfig } from "./private"; +import { Incentives } from "./private"; +import { Level } from "../adapters/supabase"; +import { CommandObj, WideLabel, WideOrgConfig, WideRepoConfig } from "./private"; interface Configs { parsedRepo?: WideRepoConfig; @@ -7,10 +8,31 @@ interface Configs { parsedDefault: WideRepoConfig; } +export const getNumericLevel = (level: Level) => { + switch (level) { + case Level.ERROR: + return 0; + case Level.WARN: + return 1; + case Level.INFO: + return 2; + case Level.HTTP: + return 3; + case Level.VERBOSE: + return 4; + case Level.DEBUG: + return 5; + case Level.SILLY: + return 6; + default: + return -1; // Invalid level + } +}; + export const getNetworkId = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): number => { - if (parsedRepo && parsedRepo["evm-network-id"] && !Number.isNaN(Number(parsedRepo["evm-network-id"]))) { + if (parsedRepo && parsedRepo["evm-network-id"] !== undefined && !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"]))) { + } else if (parsedOrg && parsedOrg["evm-network-id"] !== undefined && !Number.isNaN(Number(parsedOrg["evm-network-id"]))) { return Number(parsedOrg["evm-network-id"]); } else { return Number(parsedDefault["evm-network-id"]); @@ -18,9 +40,9 @@ export const getNetworkId = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): }; export const getBaseMultiplier = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): number => { - if (parsedRepo && parsedRepo["price-multiplier"] && !Number.isNaN(Number(parsedRepo["price-multiplier"]))) { + if (parsedRepo && parsedRepo["price-multiplier"] !== undefined && !Number.isNaN(Number(parsedRepo["price-multiplier"]))) { return Number(parsedRepo["price-multiplier"]); - } else if (parsedOrg && parsedOrg["price-multiplier"] && !Number.isNaN(Number(parsedOrg["price-multiplier"]))) { + } else if (parsedOrg && parsedOrg["price-multiplier"] !== undefined && !Number.isNaN(Number(parsedOrg["price-multiplier"]))) { return Number(parsedOrg["price-multiplier"]); } else { return Number(parsedDefault["price-multiplier"]); @@ -28,9 +50,9 @@ export const getBaseMultiplier = ({ parsedRepo, parsedOrg, parsedDefault }: Conf }; export const getCreatorMultiplier = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): number => { - if (parsedRepo && parsedRepo["issue-creator-multiplier"] && !Number.isNaN(Number(parsedRepo["issue-creator-multiplier"]))) { + if (parsedRepo && parsedRepo["issue-creator-multiplier"] !== undefined && !Number.isNaN(Number(parsedRepo["issue-creator-multiplier"]))) { return Number(parsedRepo["issue-creator-multiplier"]); - } else if (parsedOrg && parsedOrg["issue-creator-multiplier"] && !Number.isNaN(Number(parsedOrg["issue-creator-multiplier"]))) { + } else if (parsedOrg && parsedOrg["issue-creator-multiplier"] !== undefined && !Number.isNaN(Number(parsedOrg["issue-creator-multiplier"]))) { return Number(parsedOrg["issue-creator-multiplier"]); } else { return Number(parsedDefault["issue-creator-multiplier"]); @@ -38,49 +60,74 @@ export const getCreatorMultiplier = ({ parsedRepo, parsedOrg, parsedDefault }: C }; export const getTimeLabels = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): WideLabel[] => { - if (parsedRepo && parsedRepo["time-labels"] && Array.isArray(parsedRepo["time-labels"]) && parsedRepo["time-labels"].length > 0) { + if (parsedRepo && parsedRepo["time-labels"] !== undefined && Array.isArray(parsedRepo["time-labels"]) && parsedRepo["time-labels"].length > 0) { return parsedRepo["time-labels"]; - } else if (parsedOrg && parsedOrg["time-labels"] && Array.isArray(parsedOrg["time-labels"]) && parsedOrg["time-labels"].length > 0) { + } else if (parsedOrg && parsedOrg["time-labels"] !== undefined && Array.isArray(parsedOrg["time-labels"]) && parsedOrg["time-labels"].length > 0) { return parsedOrg["time-labels"]; } else { return parsedDefault["time-labels"] as WideLabel[]; } }; +export const getCommandSettings = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): CommandObj[] => { + if (parsedRepo && parsedRepo["command-settings"] && Array.isArray(parsedRepo["command-settings"]) && parsedRepo["command-settings"].length > 0) { + return parsedRepo["command-settings"]; + } else if (parsedOrg && parsedOrg["command-settings"] && Array.isArray(parsedOrg["command-settings"]) && parsedOrg["command-settings"].length > 0) { + return parsedOrg["command-settings"]; + } else { + return parsedDefault["command-settings"] as CommandObj[]; + } +}; + export const getPriorityLabels = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): WideLabel[] => { - if (parsedRepo && parsedRepo["priority-labels"] && Array.isArray(parsedRepo["priority-labels"]) && parsedRepo["priority-labels"].length > 0) { + if (parsedRepo && parsedRepo["priority-labels"] !== undefined && Array.isArray(parsedRepo["priority-labels"]) && parsedRepo["priority-labels"].length > 0) { return parsedRepo["priority-labels"]; - } else if (parsedOrg && parsedOrg["priority-labels"] && Array.isArray(parsedOrg["priority-labels"]) && parsedOrg["priority-labels"].length > 0) { + } else if ( + parsedOrg && + parsedOrg["priority-labels"] !== undefined && + Array.isArray(parsedOrg["priority-labels"]) && + parsedOrg["priority-labels"].length > 0 + ) { return parsedOrg["priority-labels"]; } else { return parsedDefault["priority-labels"] as WideLabel[]; } }; -export const getCommentItemPrice = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): CommentElementPricing => { - if (parsedRepo && parsedRepo["comment-element-pricing"]) { - return parsedRepo["comment-element-pricing"]; - } else if (parsedOrg && parsedOrg["comment-element-pricing"]) { - return parsedOrg["comment-element-pricing"]; +export const getIncentives = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): Incentives => { + if (parsedRepo && parsedRepo["incentives"]) { + return parsedRepo["incentives"]; + } else if (parsedOrg && parsedOrg["incentives"]) { + return parsedOrg["incentives"]; + } else { + return parsedDefault["incentives"] as Incentives; + } +}; + +export const getPaymentPermitMaxPrice = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): number => { + if (parsedRepo && parsedRepo["payment-permit-max-price"] && typeof parsedRepo["payment-permit-max-price"] === "number") { + return Number(parsedRepo["payment-permit-max-price"]); + } else if (parsedOrg && parsedOrg["payment-permit-max-price"] && typeof parsedOrg["payment-permit-max-price"] === "number") { + return Number(parsedOrg["payment-permit-max-price"]); } else { - return parsedDefault["comment-element-pricing"] as CommentElementPricing; + return Number(parsedDefault["payment-permit-max-price"]); } }; -export const getAutoPayMode = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): boolean => { - if (parsedRepo && parsedRepo["auto-pay-mode"] && typeof parsedRepo["auto-pay-mode"] === "boolean") { - return parsedRepo["auto-pay-mode"]; - } else if (parsedOrg && parsedOrg["auto-pay-mode"] && typeof parsedOrg["auto-pay-mode"] === "boolean") { - return parsedOrg["auto-pay-mode"]; +export const getAssistivePricing = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): boolean => { + if (parsedRepo && parsedRepo["assistive-pricing"] && typeof parsedRepo["assistive-pricing"] === "boolean") { + return parsedRepo["assistive-pricing"]; + } else if (parsedOrg && parsedOrg["assistive-pricing"] && typeof parsedOrg["assistive-pricing"] === "boolean") { + return parsedOrg["assistive-pricing"]; } else { - return parsedDefault["auto-pay-mode"] as boolean; + return parsedDefault["assistive-pricing"] as boolean; } }; export const getAnalyticsMode = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): boolean => { - if (parsedRepo && parsedRepo["disable-analytics"] && typeof parsedRepo["disable-analytics"] === "boolean") { + if (parsedRepo && parsedRepo["disable-analytics"] !== undefined && typeof parsedRepo["disable-analytics"] === "boolean") { return parsedRepo["disable-analytics"]; - } else if (parsedOrg && parsedOrg["disable-analytics"] && typeof parsedOrg["disable-analytics"] === "boolean") { + } else if (parsedOrg && parsedOrg["disable-analytics"] !== undefined && typeof parsedOrg["disable-analytics"] === "boolean") { return parsedOrg["disable-analytics"]; } else { return parsedDefault["disable-analytics"] as boolean; @@ -88,9 +135,9 @@ export const getAnalyticsMode = ({ parsedRepo, parsedOrg, parsedDefault }: Confi }; export const getPromotionComment = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): string => { - if (parsedRepo && parsedRepo["promotion-comment"] && typeof parsedRepo["promotion-comment"] === "string") { + if (parsedRepo && parsedRepo["promotion-comment"] !== undefined && typeof parsedRepo["promotion-comment"] === "string") { return parsedRepo["promotion-comment"]; - } else if (parsedOrg && parsedOrg["promotion-comment"] && typeof parsedOrg["promotion-comment"] === "string") { + } else if (parsedOrg && parsedOrg["promotion-comment"] !== undefined && typeof parsedOrg["promotion-comment"] === "string") { return parsedOrg["promotion-comment"]; } else { return parsedDefault["promotion-comment"] as string; @@ -98,9 +145,9 @@ export const getPromotionComment = ({ parsedRepo, parsedOrg, parsedDefault }: Co }; export const getIncentiveMode = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): boolean => { - if (parsedRepo && parsedRepo["comment-incentives"] && typeof parsedRepo["comment-incentives"] === "boolean") { + if (parsedRepo && parsedRepo["comment-incentives"] !== undefined && typeof parsedRepo["comment-incentives"] === "boolean") { return parsedRepo["comment-incentives"]; - } else if (parsedOrg && parsedOrg["comment-incentives"] && typeof parsedOrg["comment-incentives"] === "boolean") { + } else if (parsedOrg && parsedOrg["comment-incentives"] !== undefined && typeof parsedOrg["comment-incentives"] === "boolean") { return parsedOrg["comment-incentives"]; } else { return parsedDefault["comment-incentives"] as boolean; @@ -108,9 +155,9 @@ export const getIncentiveMode = ({ parsedRepo, parsedOrg, parsedDefault }: Confi }; export const getBountyHunterMax = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): number => { - if (parsedRepo && parsedRepo["max-concurrent-assigns"] && !Number.isNaN(Number(parsedRepo["max-concurrent-assigns"]))) { + if (parsedRepo && parsedRepo["max-concurrent-assigns"] !== undefined && !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"]))) { + } else if (parsedOrg && parsedOrg["max-concurrent-assigns"] !== undefined && !Number.isNaN(Number(parsedOrg["max-concurrent-assigns"]))) { return Number(parsedOrg["max-concurrent-assigns"]); } else { return Number(parsedDefault["max-concurrent-assigns"]); @@ -118,9 +165,9 @@ export const getBountyHunterMax = ({ parsedRepo, parsedOrg, parsedDefault }: Con }; export const getDefaultLabels = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): string[] => { - if (parsedRepo && parsedRepo["default-labels"]) { + if (parsedRepo && parsedRepo["default-labels"] !== undefined) { return parsedRepo["default-labels"]; - } else if (parsedOrg && parsedOrg["default-labels"]) { + } else if (parsedOrg && parsedOrg["default-labels"] !== undefined) { return parsedOrg["default-labels"]; } else { return parsedDefault["default-labels"] as string[]; @@ -128,9 +175,9 @@ export const getDefaultLabels = ({ parsedRepo, parsedOrg, parsedDefault }: Confi }; export const getRegisterWalletWithVerification = ({ parsedRepo, parsedOrg, parsedDefault }: Configs): boolean => { - if (parsedRepo && parsedRepo["register-wallet-with-verification"] && typeof parsedRepo["register-wallet-with-verification"] === "boolean") { + if (parsedRepo && parsedRepo["register-wallet-with-verification"] !== undefined && 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") { + } else if (parsedOrg && parsedOrg["register-wallet-with-verification"] !== undefined && typeof parsedOrg["register-wallet-with-verification"] === "boolean") { return Boolean(parsedOrg["register-wallet-with-verification"]); } else { return Boolean(parsedDefault["register-wallet-with-verification"]); diff --git a/src/utils/private.ts b/src/utils/private.ts index 0a6e98064..36ff92f8c 100644 --- a/src/utils/private.ts +++ b/src/utils/private.ts @@ -2,10 +2,9 @@ import _sodium from "libsodium-wrappers"; import YAML from "yaml"; import { Payload } from "../types"; import { Context } from "probot"; -import { readFileSync } from "fs"; import { getAnalyticsMode, - getAutoPayMode, + getPaymentPermitMaxPrice, getBaseMultiplier, getCreatorMultiplier, getBountyHunterMax, @@ -13,18 +12,22 @@ import { getNetworkId, getPriorityLabels, getTimeLabels, - getCommentItemPrice, getDefaultLabels, getPromotionComment, + getIncentives, + getAssistivePricing, + getCommandSettings, getRegisterWalletWithVerification, } from "./helpers"; +import DEFAULT_CONFIG_JSON from "../../ubiquibot-config-default.json"; + const CONFIG_REPO = "ubiquibot-config"; -const KEY_PATH = ".github/ubiquibot-config.yml"; +const CONFIG_PATH = ".github/ubiquibot-config.yml"; const KEY_NAME = "private-key-encrypted"; const KEY_PREFIX = "HSK_"; -export const getConfigSuperset = async (context: Context, type: "org" | "repo"): Promise => { +export const getConfigSuperset = async (context: Context, type: "org" | "repo", filePath: string): Promise => { try { const payload = context.payload as Payload; const repo = type === "org" ? CONFIG_REPO : payload.repository.name; @@ -33,7 +36,7 @@ export const getConfigSuperset = async (context: Context, type: "org" | "repo"): const { data } = await context.octokit.rest.repos.getContent({ owner, repo, - path: KEY_PATH, + path: filePath, mediaType: { format: "raw", }, @@ -46,8 +49,22 @@ export const getConfigSuperset = async (context: Context, type: "org" | "repo"): export interface WideLabel { name: string; - weight: number; - value?: number | undefined; +} + +export interface CommentIncentives { + elements: Record; + totals: { + word: number; + }; +} + +export interface Incentives { + comment: CommentIncentives; +} + +export interface CommandObj { + name: string; + enabled: boolean; } export interface WideConfig { @@ -56,12 +73,14 @@ export interface WideConfig { "issue-creator-multiplier": number; "time-labels"?: WideLabel[]; "priority-labels"?: WideLabel[]; - "auto-pay-mode"?: boolean; + "payment-permit-max-price"?: number; + "command-settings"?: CommandObj[]; "promotion-comment"?: string; "disable-analytics"?: boolean; "comment-incentives"?: boolean; + "assistive-pricing"?: boolean; "max-concurrent-assigns"?: number; - "comment-element-pricing"?: Record; + incentives?: Incentives; "default-labels"?: string[]; "register-wallet-with-verification"?: boolean; } @@ -84,9 +103,16 @@ export const parseYAML = (data?: string): WideConfig | undefined => { } }; -export const getDefaultConfig = (): WideRepoConfig => { - const defaultConfig = readFileSync(`${__dirname}/../../ubiquibot-config-default.yml`, "utf8"); - return parseYAML(defaultConfig) as WideRepoConfig; +export const getOrgAndRepoFromPath = (path: string) => { + const parts = path.split("/"); + + if (parts.length !== 2) { + return { org: null, repo: null }; + } + + const [org, repo] = parts; + + return { org, repo }; }; export const getPrivateKey = async (cipherText: string): Promise => { @@ -130,27 +156,29 @@ export const getScalarKey = async (X25519_PRIVATE_KEY: string | undefined): Prom }; export const getWideConfig = async (context: Context) => { - const orgConfig = await getConfigSuperset(context, "org"); - const repoConfig = await getConfigSuperset(context, "repo"); + const orgConfig = await getConfigSuperset(context, "org", CONFIG_PATH); + const repoConfig = await getConfigSuperset(context, "repo", CONFIG_PATH); const parsedOrg: WideOrgConfig | undefined = parseYAML(orgConfig); const parsedRepo: WideRepoConfig | undefined = parseYAML(repoConfig); - const parsedDefault: WideRepoConfig = getDefaultConfig(); + const parsedDefault: WideRepoConfig = DEFAULT_CONFIG_JSON; const privateKeyDecrypted = parsedOrg && parsedOrg[KEY_NAME] ? await getPrivateKey(parsedOrg[KEY_NAME]) : undefined; const configs = { parsedRepo, parsedOrg, parsedDefault }; const configData = { networkId: getNetworkId(configs), privateKey: privateKeyDecrypted ?? "", + assistivePricing: getAssistivePricing(configs), + commandSettings: getCommandSettings(configs), baseMultiplier: getBaseMultiplier(configs), issueCreatorMultiplier: getCreatorMultiplier(configs), timeLabels: getTimeLabels(configs), priorityLabels: getPriorityLabels(configs), - autoPayMode: getAutoPayMode(configs), + paymentPermitMaxPrice: getPaymentPermitMaxPrice(configs), disableAnalytics: getAnalyticsMode(configs), bountyHunterMax: getBountyHunterMax(configs), incentiveMode: getIncentiveMode(configs), - commentElementPricing: getCommentItemPrice(configs), + incentives: getIncentives(configs), defaultLabels: getDefaultLabels(configs), promotionComment: getPromotionComment(configs), registerWalletWithVerification: getRegisterWalletWithVerification(configs), diff --git a/supabase/README.md b/supabase/README.md index bdfa8ab90..397affb3f 100644 --- a/supabase/README.md +++ b/supabase/README.md @@ -56,5 +56,6 @@ For more information about arguments, please go through [here](https://supabase. ### Database Operation - `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 migration repair --status reverted`: Revert a given migration file. +- `supabase db push`: Update database schema on supabase platform - `supabase gen types typescript > src/adapters/supabase/types/database.ts --linked`: Generate typescript types from the supabase project linked diff --git a/supabase/migrations/20230730153700_permit.sql b/supabase/migrations/20230730153700_permit.sql new file mode 100644 index 000000000..8fbfb87ed --- /dev/null +++ b/supabase/migrations/20230730153700_permit.sql @@ -0,0 +1,17 @@ + +CREATE TABLE IF NOT EXISTS permits ( + id bigint PRIMARY KEY GENERATED BY DEFAULT AS IDENTITY, -- auto-incrementing unique id + created_at timestamptz NOT NULL, + organization_id bigint, + repository_id bigint NOT NULL, + issue_id bigint NOT NULL, + network_id int NOT NULL, + bounty_hunter_id bigint NOT NULL, + bounty_hunter_address text NOT NULL, + token_address text NOT NULL, + payout_amount text NOT NULL, + nonce text NOT NULL, + deadline text NOT NULL, + signature text NOT NULL, + wallet_owner_address text NOT NULL +); \ No newline at end of file diff --git a/supabase/migrations/20230803154507_setup_permits_policies.sql b/supabase/migrations/20230803154507_setup_permits_policies.sql new file mode 100644 index 000000000..7c2828580 --- /dev/null +++ b/supabase/migrations/20230803154507_setup_permits_policies.sql @@ -0,0 +1,13 @@ +ALTER TABLE permits ENABLE ROW LEVEL SECURITY; +ALTER TABLE wallets ENABLE ROW LEVEL SECURITY; +ALTER TABLE users ENABLE ROW LEVEL SECURITY; +ALTER TABLE issues ENABLE ROW LEVEL SECURITY; +ALTER TABLE weekly ENABLE ROW LEVEL SECURITY; +ALTER TABLE access ENABLE ROW LEVEL SECURITY; +ALTER TABLE penalty ENABLE ROW LEVEL SECURITY; +ALTER TABLE multiplier ENABLE ROW LEVEL SECURITY; + +CREATE POLICY "Enable read access for frontend" ON "public"."permits" +AS PERMISSIVE FOR SELECT +TO public +USING (true) \ No newline at end of file diff --git a/supabase/migrations/20230818030324_create_logs.sql b/supabase/migrations/20230818030324_create_logs.sql new file mode 100644 index 000000000..2bc8647c2 --- /dev/null +++ b/supabase/migrations/20230818030324_create_logs.sql @@ -0,0 +1,16 @@ +-- Create `logs` table +CREATE TABLE logs ( + id SERIAL PRIMARY KEY, + repo_name TEXT, + org_name TEXT, + comment_id INT, + issue_number INT, + log_message TEXT, + level INT, + timestamp TIMESTAMPTZ DEFAULT current_timestamp +); + +CREATE INDEX idx_timestamp ON logs (timestamp); + +-- Enable RLS for `logs` table +ALTER TABLE logs ENABLE ROW LEVEL SECURITY \ No newline at end of file diff --git a/supabase/migrations/20230826183500_change_wallet_type.sql b/supabase/migrations/20230826183500_change_wallet_type.sql new file mode 100644 index 000000000..bdf90e17c --- /dev/null +++ b/supabase/migrations/20230826183500_change_wallet_type.sql @@ -0,0 +1,4 @@ +ALTER TABLE wallets ALTER COLUMN wallet_address TYPE text; + +ALTER TABLE access DROP CONSTRAINT access_pkey; +ALTER TABLE access ADD CONSTRAINT access_pkey PRIMARY KEY (user_name, repository); \ No newline at end of file diff --git a/supabase/migrations/20230829101134_setup_cron.sql b/supabase/migrations/20230829101134_setup_cron.sql new file mode 100644 index 000000000..883436a87 --- /dev/null +++ b/supabase/migrations/20230829101134_setup_cron.sql @@ -0,0 +1,11 @@ +-- Enable the pg_cron extension +CREATE EXTENSION IF NOT EXISTS pg_cron; + +-- Runs everyday at 03:00 AM to cleanup logs that are older than a week +-- Use the cron time format to modify the trigger time if necessary +SELECT + cron.schedule( + 'logs-cleaner', -- Job name + '0 3 * * *', -- Everyday at 03:00 AM + $$DELETE FROM logs WHERE timestamp < now() - INTERVAL '1 week'$$ + ); diff --git a/tsconfig.json b/tsconfig.json index c9c5da1c6..6dacfb57e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -22,7 +22,6 @@ // "importHelpers": true, /* Import emit helpers from 'tslib'. */ // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ - /* Strict Type-Checking Options */ "strict": true /* Enable all strict type-checking options. */, // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ @@ -32,13 +31,11 @@ // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ - /* Additional Checks */ "noUnusedLocals": true /* Report errors on unused locals. */, "noUnusedParameters": true /* Report errors on unused parameters. */, "noImplicitReturns": true /* Report error when not all code paths in function return a value. */, "noFallthroughCasesInSwitch": true /* Report errors for fallthrough cases in switch statement. */, - /* Module Resolution Options */ "moduleResolution": "node" /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */, // "baseUrl": "./src" /* Base directory to resolve non-absolute module names. */, @@ -50,17 +47,14 @@ "esModuleInterop": true /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */, // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ - /* Source Map Options */ // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ - /* Experimental Options */ "experimentalDecorators": true /* Enables experimental support for ES7 decorators. */, "emitDecoratorMetadata": true /* Enables experimental support for emitting type metadata for decorators. */, - /* Advanced Options */ "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */, "resolveJsonModule": true, diff --git a/ubiquibot-config-default.json b/ubiquibot-config-default.json new file mode 100644 index 000000000..2cf139e15 --- /dev/null +++ b/ubiquibot-config-default.json @@ -0,0 +1,89 @@ +{ + "evm-network-id": 100, + "price-multiplier": 1, + "issue-creator-multiplier": 2, + "payment-permit-max-price": 9007199254740991, + "max-concurrent-assigns": 9007199254740991, + "assistive-pricing": false, + "disable-analytics": false, + "comment-incentives": false, + "register-wallet-with-verification": false, + "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!
    ", + "default-labels": [], + "time-labels": [ + { + "name": "Time: <1 Hour" + }, + { + "name": "Time: <1 Day" + }, + { + "name": "Time: <1 Week" + }, + { + "name": "Time: <2 Weeks" + }, + { + "name": "Time: <1 Month" + } + ], + "priority-labels": [ + { + "name": "Priority: 1 (Normal)" + }, + { + "name": "Priority: 2 (Medium)" + }, + { + "name": "Priority: 3 (High)" + }, + { + "name": "Priority: 4 (Urgent)" + }, + { + "name": "Priority: 5 (Emergency)" + } + ], + "command-settings": [ + { + "name": "start", + "enabled": false + }, + { + "name": "stop", + "enabled": false + }, + { + "name": "wallet", + "enabled": false + }, + { + "name": "payout", + "enabled": false + }, + { + "name": "multiplier", + "enabled": false + }, + { + "name": "query", + "enabled": false + }, + { + "name": "allow", + "enabled": false + }, + { + "name": "autopay", + "enabled": false + } + ], + "incentives": { + "comment": { + "elements": {}, + "totals": { + "word": 0 + } + } + } +} diff --git a/ubiquibot-config-default.yml b/ubiquibot-config-default.yml deleted file mode 100644 index 5b0c70ac9..000000000 --- a/ubiquibot-config-default.yml +++ /dev/null @@ -1,44 +0,0 @@ ---- -evm-network-id: 1 -price-multiplier: 1000 -issue-creator-multiplier: 2000 -time-labels: - - name: "Time: <1 Hour" - weight: 0.125 - value: 3600 - - name: "Time: <1 Day" - weight: 1 - value: 86400 - - name: "Time: <1 Week" - weight: 2 - value: 604800 - - name: "Time: <2 Weeks" - weight: 3 - value: 1209600 - - name: "Time: <1 Month" - weight: 4 - value: 2592000 -priority-labels: - - name: "Priority: 0 (Normal)" - weight: 1 - - name: "Priority: 1 (Medium)" - weight: 2 - - name: "Priority: 2 (High)" - weight: 3 - - name: "Priority: 3 (Urgent)" - weight: 4 - - name: "Priority: 4 (Emergency)" - weight: 5 -auto-pay-mode: true -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 -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 diff --git a/yarn.lock b/yarn.lock index 671f8c351..2d930cf18 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1529,22 +1529,6 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@logdna/logger@^2.6.6": - version "2.6.7" - resolved "https://registry.yarnpkg.com/@logdna/logger/-/logger-2.6.7.tgz#9a1f5ea0c69b373f223319d956eb96042df383ec" - integrity sha512-Jn7CLP85yjfe5cBDO/ExtjPOE5ssG7lsBtMWo4ubb3sG8yjkBufJIIerCXLh5jG+otLOfh6CKGudOmOFak7PYg== - dependencies: - "@logdna/stdlib" "^1.1.5" - agentkeepalive "^4.1.3" - axios "^0.25.0" - https-proxy-agent "^5.0.0" - json-stringify-safe "^5.0.1" - -"@logdna/stdlib@^1.1.5": - version "1.2.3" - resolved "https://registry.yarnpkg.com/@logdna/stdlib/-/stdlib-1.2.3.tgz#a95b4cc74f0c4378769e41c06b69151058d6a26e" - integrity sha512-67aauh8AY395vtgAADIPZTHjDtiCJBrNb2sM13zb2lWRJWAPVdiEBcgQValDR4OuwOdkr4kAMiTMBuMziATKiA== - "@netlify/functions@^1.4.0": version "1.6.0" resolved "https://registry.yarnpkg.com/@netlify/functions/-/functions-1.6.0.tgz#c373423e6fef0e6f7422ac0345e8bbf2cb692366" @@ -1970,17 +1954,17 @@ dependencies: cross-fetch "^3.1.5" -"@supabase/gotrue-js@^2.31.0": - version "2.32.0" - resolved "https://registry.yarnpkg.com/@supabase/gotrue-js/-/gotrue-js-2.32.0.tgz#806afef38cd7a7b92300194f844fb56e6873ca74" - integrity sha512-H9yfM1RF1wM887+5zDqITagfBaZp8qzvXF2/hoIZQMYE+jLbLiCk5ncoS4CWLY4AwKD5NnQS/8dUGFTHQcxqDA== +"@supabase/gotrue-js@^2.46.1": + version "2.47.0" + resolved "https://registry.yarnpkg.com/@supabase/gotrue-js/-/gotrue-js-2.47.0.tgz#67cca8f7be726fcfcc6dd49f515bbb20b2278f74" + integrity sha512-3e34/vsKH/DoSZCpB85UZpFWSJ2p4GRUUlqgAgeTPagPlx4xS+Nc5v7g7ic7vp3gK0J5PsYVCn9Qu2JQUp4vXg== dependencies: cross-fetch "^3.1.5" -"@supabase/postgrest-js@^1.7.0": - version "1.7.1" - resolved "https://registry.yarnpkg.com/@supabase/postgrest-js/-/postgrest-js-1.7.1.tgz#12158937783bdc750ada705d7135d01f2fc96196" - integrity sha512-xPRYLaZrkLbXNlzmHW6Wtf9hmcBLjjI5xUz2zj8oE2hgXGaYoZBBkpN9bmW9i17Z1f6Ujxa942AqK439XOA36A== +"@supabase/postgrest-js@^1.8.0": + version "1.8.0" + resolved "https://registry.yarnpkg.com/@supabase/postgrest-js/-/postgrest-js-1.8.0.tgz#c10b65e120c6523fc947225bcb9131869cf509f1" + integrity sha512-R6leDIC92NgjyG2/tCRJ42rWN7+fZY6ulTEE+c00tcnghn6cX4IYUlnTNMtrdfYC2JYNOTyM+rWj63Wdhr7Zig== dependencies: cross-fetch "^3.1.5" @@ -2000,14 +1984,14 @@ dependencies: cross-fetch "^3.1.5" -"@supabase/supabase-js@^2.4.0": - version "2.26.0" - resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.26.0.tgz#80eb5d7b6dc5ac7b29218f7211747e256184247a" - integrity sha512-RXmTPTobaYAwkSobadHZmEVLmzX3SGrtRZIGfLWnLv92VzBRrjuXn0a+bJqKl50GUzsyqPA+j5pod7EwMkcH5A== +"@supabase/supabase-js@^2.32.0": + version "2.32.0" + resolved "https://registry.yarnpkg.com/@supabase/supabase-js/-/supabase-js-2.32.0.tgz#863c636d83232c6a2e9ba5932e0d7c1bf80bc436" + integrity sha512-1ShFhuOI5Du7604nlCelBsRD61daXk2O0qwjumoz35bqrYThnSPPtpJqZOHw6Mg6o7mLjIInYLh/DBlh8UvzRg== dependencies: "@supabase/functions-js" "^2.1.0" - "@supabase/gotrue-js" "^2.31.0" - "@supabase/postgrest-js" "^1.7.0" + "@supabase/gotrue-js" "^2.46.1" + "@supabase/postgrest-js" "^1.8.0" "@supabase/realtime-js" "^2.7.3" "@supabase/storage-js" "^2.5.1" cross-fetch "^3.1.5" @@ -2100,13 +2084,6 @@ dependencies: "@types/node" "*" -"@types/debug@^4.0.0": - version "4.1.8" - resolved "https://registry.yarnpkg.com/@types/debug/-/debug-4.1.8.tgz#cef723a5d0a90990313faec2d1e22aee5eecb317" - integrity sha512-/vPO1EPOs306Cvhwv7KfVfYvOJqA/S/AXjaHQiJboCZzcNDb+TIJFN9/2C9DZ//ijSKWioNyUxD792QmDJ+HKQ== - dependencies: - "@types/ms" "*" - "@types/eslint@^8.40.2": version "8.40.2" resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.40.2.tgz#2833bc112d809677864a4b0e7d1de4f04d7dac2d" @@ -2213,13 +2190,6 @@ resolved "https://registry.yarnpkg.com/@types/libsodium-wrappers/-/libsodium-wrappers-0.7.10.tgz#a6ebde70d3b4af960fd802af8d0e3c7cfe281eb2" integrity sha512-BqI9B92u+cM3ccp8mpHf+HzJ8fBlRwdmyd6+fz3p99m3V6ifT5O3zmOMi612PGkpeFeG/G6loxUnzlDNhfjPSA== -"@types/mdast@^3.0.0", "@types/mdast@^3.0.11": - version "3.0.11" - resolved "https://registry.yarnpkg.com/@types/mdast/-/mdast-3.0.11.tgz#dc130f7e7d9306124286f6d6cee40cf4d14a3dc0" - integrity sha512-Y/uImid8aAwrEA24/1tcRZwpxX3pIFTSilcNDKSPn+Y2iDywSEachzRuvgAYYLR3wpGXAsMbv5lvKLDZLeYPAw== - dependencies: - "@types/unist" "*" - "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" @@ -2235,7 +2205,7 @@ resolved "https://registry.yarnpkg.com/@types/minimist/-/minimist-1.2.2.tgz#ee771e2ba4b3dc5b372935d549fd9617bf345b8c" integrity sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ== -"@types/ms@*", "@types/ms@^0.7.31": +"@types/ms@^0.7.31": version "0.7.31" resolved "https://registry.yarnpkg.com/@types/ms/-/ms-0.7.31.tgz#31b7ca6407128a3d2bbc27fe2d21b345397f6197" integrity sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA== @@ -2345,11 +2315,6 @@ resolved "https://registry.yarnpkg.com/@types/stack-utils/-/stack-utils-2.0.1.tgz#20f18294f797f2209b5f65c8e3b5c8e8261d127c" integrity sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw== -"@types/unist@*", "@types/unist@^2.0.0": - version "2.0.6" - resolved "https://registry.yarnpkg.com/@types/unist/-/unist-2.0.6.tgz#250a7b16c3b91f672a24552ec64678eeb1d3a08d" - integrity sha512-PBjIUxZHOuj0R15/xuwJYjFi+KZdNFrehocChv4g5hu6aFroHue8m0lBP0POdK2nKzbw0cgV1mws8+V/JAcEkQ== - "@types/websocket@^1.0.3": version "1.0.5" resolved "https://registry.yarnpkg.com/@types/websocket/-/websocket-1.0.5.tgz#3fb80ed8e07f88e51961211cd3682a3a4a81569c" @@ -2558,15 +2523,6 @@ agent-base@6: dependencies: debug "4" -agentkeepalive@^4.1.3: - version "4.3.0" - resolved "https://registry.yarnpkg.com/agentkeepalive/-/agentkeepalive-4.3.0.tgz#bb999ff07412653c1803b3ced35e50729830a255" - integrity sha512-7Epl1Blf4Sy37j4v9f9FjICCh4+KAQOyXgHEwlyBiAQLbhKdq/i2QQU3amQalS/wPhdPzDXPL5DMR5bkn+YeWg== - dependencies: - debug "^4.1.0" - depd "^2.0.0" - humanize-ms "^1.2.1" - aggregate-error@^3.0.0, aggregate-error@^3.1.0: version "3.1.0" resolved "https://registry.yarnpkg.com/aggregate-error/-/aggregate-error-3.1.0.tgz#92670ff50f5359bdb7a3e0d40d0ec30c5737687a" @@ -2761,13 +2717,6 @@ atomic-sleep@^1.0.0: resolved "https://registry.yarnpkg.com/atomic-sleep/-/atomic-sleep-1.0.0.tgz#eb85b77a601fc932cfe432c5acd364a9e2c9075b" integrity sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ== -axios@^0.25.0: - version "0.25.0" - resolved "https://registry.yarnpkg.com/axios/-/axios-0.25.0.tgz#349cfbb31331a9b4453190791760a8d35b093e0a" - integrity sha512-cD8FOb0tRH3uuEe6+evtAbgJtfxr7ly3fQjYcMcuPlgkwVS9xboaVIpcDV+cYQe+yGykgwZCs1pzjntcGa6l5g== - dependencies: - follow-redirects "^1.14.7" - axios@^1.3.2: version "1.4.0" resolved "https://registry.yarnpkg.com/axios/-/axios-1.4.0.tgz#38a7bf1224cd308de271146038b551d725f0be1f" @@ -3152,11 +3101,6 @@ capture-exit@^2.0.0: dependencies: rsvp "^4.8.4" -ccount@^2.0.0: - version "2.0.1" - resolved "https://registry.yarnpkg.com/ccount/-/ccount-2.0.1.tgz#17a3bf82302e0870d6da43a01311a8bc02a3ecf5" - integrity sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg== - chalk@2.4.2, chalk@^2.0.0: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" @@ -3184,11 +3128,6 @@ char-regex@^1.0.2: resolved "https://registry.yarnpkg.com/char-regex/-/char-regex-1.0.2.tgz#d744358226217f981ed58f479b1d6bcc29545dcf" integrity sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw== -character-entities@^2.0.0: - version "2.0.2" - resolved "https://registry.yarnpkg.com/character-entities/-/character-entities-2.0.2.tgz#2d09c2e72cd9523076ccb21157dff66ad43fcc22" - integrity sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ== - chokidar@^3.5.2: version "3.5.3" resolved "https://registry.yarnpkg.com/chokidar/-/chokidar-3.5.3.tgz#1cf37c8707b932bd1af1ae22c0432e2acd1903bd" @@ -3276,6 +3215,15 @@ cliui@^6.0.0: strip-ansi "^6.0.0" wrap-ansi "^6.2.0" +cliui@^7.0.2: + version "7.0.4" + resolved "https://registry.yarnpkg.com/cliui/-/cliui-7.0.4.tgz#a0265ee655476fc807aea9df3df8df7783808b4f" + integrity sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ== + dependencies: + string-width "^4.2.0" + strip-ansi "^6.0.0" + wrap-ansi "^7.0.0" + cliui@^8.0.1: version "8.0.1" resolved "https://registry.yarnpkg.com/cliui/-/cliui-8.0.1.tgz#0c04b075db02cbfe60dc8e6cf2f5486b1a3608aa" @@ -3458,6 +3406,24 @@ copy-descriptor@^0.1.0: resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" integrity sha512-XgZ0pFcakEUlbwQEVNg3+QAis1FyTL3Qel9FYy8pSkQqoG3PNoT0bOCQtOXcOkur21r2Eq2kI+IE+gsmAEVlYw== +copyfiles@^2.4.1: + version "2.4.1" + resolved "https://registry.yarnpkg.com/copyfiles/-/copyfiles-2.4.1.tgz#d2dcff60aaad1015f09d0b66e7f0f1c5cd3c5da5" + integrity sha512-fereAvAvxDrQDOXybk3Qu3dPbOoKoysFMWtkY3mv5BsL8//OSZVL5DCLYqgRfY5cWirgRzlC+WSrxp6Bo3eNZg== + dependencies: + glob "^7.0.5" + minimatch "^3.0.3" + mkdirp "^1.0.4" + noms "0.0.0" + through2 "^2.0.1" + untildify "^4.0.0" + yargs "^16.1.0" + +core-util-is@~1.0.0: + version "1.0.3" + resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85" + integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ== + cosmiconfig-typescript-loader@^4.0.0: version "4.3.0" resolved "https://registry.yarnpkg.com/cosmiconfig-typescript-loader/-/cosmiconfig-typescript-loader-4.3.0.tgz#c4259ce474c9df0f32274ed162c0447c951ef073" @@ -3584,7 +3550,7 @@ debug@2.6.9, debug@^2.2.0, debug@^2.3.3: dependencies: ms "2.0.0" -debug@4, debug@4.3.4, debug@^4.0.0, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: +debug@4, debug@4.3.4, debug@^4.1.0, debug@^4.1.1, debug@^4.3.1, debug@^4.3.2, debug@^4.3.3, debug@^4.3.4: version "4.3.4" resolved "https://registry.yarnpkg.com/debug/-/debug-4.3.4.tgz#1319f6579357f2338d3337d2cdd4914bb5dcc865" integrity sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ== @@ -3611,18 +3577,11 @@ decamelize@^1.1.0, decamelize@^1.2.0: resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290" integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA== -decimal.js@^10.2.1: +decimal.js@^10.2.1, decimal.js@^10.4.3: version "10.4.3" resolved "https://registry.yarnpkg.com/decimal.js/-/decimal.js-10.4.3.tgz#1044092884d245d1b7f65725fa4ad4c6f781cc23" integrity sha512-VBBaLc1MgL5XpzgIP7ny5Z6Nx3UrRkIViUkPUdtl9aya5amy3De1gsUUSB1g3+3sExYNjCAsAznmukyxCb1GRA== -decode-named-character-reference@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/decode-named-character-reference/-/decode-named-character-reference-1.0.2.tgz#daabac9690874c394c81e4162a0304b35d824f0e" - integrity sha512-O8x12RzrUF8xyVcY0KJowWsmaJxQbmy0/EtnNtHRpsOcT7dFk5W598coHqBVpmWo1oQQfsCqfCmkZN5DJrZVdg== - dependencies: - character-entities "^2.0.0" - decode-uri-component@^0.2.0: version "0.2.2" resolved "https://registry.yarnpkg.com/decode-uri-component/-/decode-uri-component-0.2.2.tgz#e69dbe25d37941171dd540e024c444cd5188e1e9" @@ -3670,7 +3629,7 @@ denque@^1.1.0: resolved "https://registry.yarnpkg.com/denque/-/denque-1.5.1.tgz#07f670e29c9a78f8faecb2566a1e2c11929c5cbf" integrity sha512-XwE+iZ4D6ZUB7mfYRMb5wByE8L74HCn30FBN7sWnXksWc1LO1bPDl67pBR9o/kC4z/xSNAwkMYcGgqDV3BE3Hw== -depd@2.0.0, depd@^2.0.0, depd@~2.0.0: +depd@2.0.0, depd@~2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/depd/-/depd-2.0.0.tgz#b696163cc757560d09cf22cc8fad1571b79e76df" integrity sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw== @@ -3680,11 +3639,6 @@ deprecation@^2.0.0, deprecation@^2.3.1: resolved "https://registry.yarnpkg.com/deprecation/-/deprecation-2.3.1.tgz#6368cbdb40abf3373b525ac87e4a260c3a700919" integrity sha512-xmHIy4F3scKVwMsQ4WnVaS8bHOx0DmVwRywosKhaILI0ywMDWPtBSku2HNxRvF7jtwDRsoEwYQSfbxj8b7RlJQ== -dequal@^2.0.0: - version "2.0.3" - resolved "https://registry.yarnpkg.com/dequal/-/dequal-2.0.3.tgz#2644214f1997d39ed0ee0ece72335490a7ac67be" - integrity sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA== - destroy@1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/destroy/-/destroy-1.2.0.tgz#4803735509ad8be552934c67df614f94e66fa015" @@ -3723,11 +3677,6 @@ diff@^4.0.1: resolved "https://registry.yarnpkg.com/diff/-/diff-4.0.2.tgz#60f3aecb89d5fae520c11aa19efc2bb982aade7d" integrity sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A== -diff@^5.0.0: - version "5.1.0" - resolved "https://registry.yarnpkg.com/diff/-/diff-5.1.0.tgz#bc52d298c5ea8df9194800224445ed43ffc87e40" - integrity sha512-D+mk+qE8VC/PAUrlAU34N+VfXev0ghe5ywmpqrawphmVZc1bEfn56uo9qpyGp1p4xpzOHkSW4ztBd6L7Xx4ACw== - dir-glob@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/dir-glob/-/dir-glob-3.0.1.tgz#56dbf73d992a4a93ba1584f4534063fd2e41717f" @@ -3858,7 +3807,7 @@ end-of-stream@^1.1.0, end-of-stream@^1.4.1: dependencies: once "^1.4.0" -entities@^4.2.0: +entities@^4.2.0, entities@^4.4.0: version "4.5.0" resolved "https://registry.yarnpkg.com/entities/-/entities-4.5.0.tgz#5d268ea5e7113ec74c4d033b79ea5a35a488fb48" integrity sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw== @@ -3949,11 +3898,6 @@ escape-string-regexp@^4.0.0: resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz#14ba83a5d373e3d311e5afca29cf5bfad965bf34" integrity sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA== -escape-string-regexp@^5.0.0: - version "5.0.0" - resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz#4683126b500b61762f2dbebace1806e8be31b1c8" - integrity sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw== - escodegen@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/escodegen/-/escodegen-2.0.0.tgz#5e32b12833e8aa8fa35e1bf0befa89380484c7dd" @@ -4502,7 +4446,7 @@ flatted@^3.1.0: resolved "https://registry.yarnpkg.com/flatted/-/flatted-3.2.7.tgz#609f39207cb614b89d0765b477cb2d437fbf9787" integrity sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ== -follow-redirects@^1.14.7, follow-redirects@^1.15.0: +follow-redirects@^1.15.0: version "1.15.2" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.15.2.tgz#b460864144ba63f2681096f274c4e57026da2c13" integrity sha512-VQLG33o04KaQ8uYi2tVNbdrWp1QWxNNea+nmIB4EVM28v0hmP17z7aG1+wAkNzVq4KeXTq3221ye5qTJP91JwA== @@ -4689,7 +4633,7 @@ glob-parent@^6.0.2: dependencies: is-glob "^4.0.3" -glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: +glob@^7.0.5, glob@^7.1.1, glob@^7.1.2, glob@^7.1.3, glob@^7.1.4: version "7.2.3" resolved "https://registry.yarnpkg.com/glob/-/glob-7.2.3.tgz#b8df0fb802bbfa8e89bd1d938b4e16578ed44f2b" integrity sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q== @@ -4948,13 +4892,6 @@ human-signals@^4.3.0: resolved "https://registry.yarnpkg.com/human-signals/-/human-signals-4.3.1.tgz#ab7f811e851fca97ffbd2c1fe9a958964de321b2" integrity sha512-nZXjEF2nbo7lIw3mgYjItAfgQXog3OjJogSbKa2CQIIvSGWcKgeJnQlNXip6NglNzYH45nSRiEVimMvYL8DDqQ== -humanize-ms@^1.2.1: - version "1.2.1" - resolved "https://registry.yarnpkg.com/humanize-ms/-/humanize-ms-1.2.1.tgz#c46e3159a293f6b896da29316d8b6fe8bb79bbed" - integrity sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ== - dependencies: - ms "^2.0.0" - husky@^8.0.2: version "8.0.3" resolved "https://registry.yarnpkg.com/husky/-/husky-8.0.3.tgz#4936d7212e46d1dea28fef29bb3a108872cd9184" @@ -5023,7 +4960,7 @@ inflight@^1.0.4: once "^1.3.0" wrappy "1" -inherits@2, inherits@2.0.4, inherits@^2.0.3, inherits@^2.0.4: +inherits@2, inherits@2.0.4, inherits@^2.0.1, inherits@^2.0.3, inherits@^2.0.4, inherits@~2.0.1, inherits@~2.0.3: version "2.0.4" resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c" integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ== @@ -5274,7 +5211,12 @@ is-wsl@^2.2.0: dependencies: is-docker "^2.0.0" -isarray@1.0.0: +isarray@0.0.1: + version "0.0.1" + resolved "https://registry.yarnpkg.com/isarray/-/isarray-0.0.1.tgz#8a18acfca9a8f4177e09abfc6038939b05d1eedf" + integrity sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ== + +isarray@1.0.0, isarray@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11" integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ== @@ -5974,11 +5916,6 @@ kleur@^3.0.3: resolved "https://registry.yarnpkg.com/kleur/-/kleur-3.0.3.tgz#a79c9ecc86ee1ce3fa6206d1216c501f147fc07e" integrity sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w== -kleur@^4.0.3: - version "4.1.5" - resolved "https://registry.yarnpkg.com/kleur/-/kleur-4.1.5.tgz#95106101795f7050c6c650f350c683febddb1780" - integrity sha512-o+NO+8WrRiQEE4/7nwRJhN1HWpVmJm511pBHUxPLtp0BUISzlBplORYSmTclCnJvQq2tKu/sgl3xVpkc7ZWuQQ== - leven@2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/leven/-/leven-2.1.0.tgz#c2e7a9f772094dee9d34202ae8acce4687875580" @@ -6187,11 +6124,6 @@ log-update@^4.0.0: slice-ansi "^4.0.0" wrap-ansi "^6.2.0" -longest-streak@^3.0.0: - version "3.1.0" - resolved "https://registry.yarnpkg.com/longest-streak/-/longest-streak-3.1.0.tgz#62fa67cd958742a1574af9f39866364102d90cd4" - integrity sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g== - lowercase-keys@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-2.0.0.tgz#2603e78b7b4b0006cbca2fbcc8a3202558ac9479" @@ -6262,126 +6194,6 @@ map-visit@^1.0.0: dependencies: object-visit "^1.0.0" -markdown-table@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/markdown-table/-/markdown-table-3.0.3.tgz#e6331d30e493127e031dd385488b5bd326e4a6bd" - integrity sha512-Z1NL3Tb1M9wH4XESsCDEksWoKTdlUafKc4pt0GRwjUyXaCFZ+dc3g2erqB6zm3szA2IUSi7VnPI+o/9jnxh9hw== - -mdast-util-find-and-replace@^2.0.0: - version "2.2.2" - resolved "https://registry.yarnpkg.com/mdast-util-find-and-replace/-/mdast-util-find-and-replace-2.2.2.tgz#cc2b774f7f3630da4bd592f61966fecade8b99b1" - integrity sha512-MTtdFRz/eMDHXzeK6W3dO7mXUlF82Gom4y0oOgvHhh/HXZAGvIQDUvQ0SuUx+j2tv44b8xTHOm8K/9OoRFnXKw== - dependencies: - "@types/mdast" "^3.0.0" - escape-string-regexp "^5.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^5.0.0" - -mdast-util-from-markdown@^1.0.0, mdast-util-from-markdown@^1.3.0: - version "1.3.1" - resolved "https://registry.yarnpkg.com/mdast-util-from-markdown/-/mdast-util-from-markdown-1.3.1.tgz#9421a5a247f10d31d2faed2a30df5ec89ceafcf0" - integrity sha512-4xTO/M8c82qBcnQc1tgpNtubGUW/Y1tBQ1B0i5CtSoelOLKFYlElIr3bvgREYYO5iRqbMY1YuqZng0GVOI8Qww== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - decode-named-character-reference "^1.0.0" - mdast-util-to-string "^3.1.0" - micromark "^3.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-decode-string "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - unist-util-stringify-position "^3.0.0" - uvu "^0.5.0" - -mdast-util-gfm-autolink-literal@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-1.0.3.tgz#67a13abe813d7eba350453a5333ae1bc0ec05c06" - integrity sha512-My8KJ57FYEy2W2LyNom4n3E7hKTuQk/0SES0u16tjA9Z3oFkF4RrC/hPAPgjlSpezsOvI8ObcXcElo92wn5IGA== - dependencies: - "@types/mdast" "^3.0.0" - ccount "^2.0.0" - mdast-util-find-and-replace "^2.0.0" - micromark-util-character "^1.0.0" - -mdast-util-gfm-footnote@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-1.0.2.tgz#ce5e49b639c44de68d5bf5399877a14d5020424e" - integrity sha512-56D19KOGbE00uKVj3sgIykpwKL179QsVFwx/DCW0u/0+URsryacI4MAdNJl0dh+u2PSsD9FtxPFbHCzJ78qJFQ== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" - micromark-util-normalize-identifier "^1.0.0" - -mdast-util-gfm-strikethrough@^1.0.0: - version "1.0.3" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-1.0.3.tgz#5470eb105b483f7746b8805b9b989342085795b7" - integrity sha512-DAPhYzTYrRcXdMjUtUjKvW9z/FNAMTdU0ORyMcbmkwYNbKocDpdk+PX1L1dQgOID/+vVs1uBQ7ElrBQfZ0cuiQ== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" - -mdast-util-gfm-table@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-table/-/mdast-util-gfm-table-1.0.7.tgz#3552153a146379f0f9c4c1101b071d70bbed1a46" - integrity sha512-jjcpmNnQvrmN5Vx7y7lEc2iIOEytYv7rTvu+MeyAsSHTASGCCRA79Igg2uKssgOs1i1po8s3plW0sTu1wkkLGg== - dependencies: - "@types/mdast" "^3.0.0" - markdown-table "^3.0.0" - mdast-util-from-markdown "^1.0.0" - mdast-util-to-markdown "^1.3.0" - -mdast-util-gfm-task-list-item@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-1.0.2.tgz#b280fcf3b7be6fd0cc012bbe67a59831eb34097b" - integrity sha512-PFTA1gzfp1B1UaiJVyhJZA1rm0+Tzn690frc/L8vNX1Jop4STZgOE6bxUhnzdVSB+vm2GU1tIsuQcA9bxTQpMQ== - dependencies: - "@types/mdast" "^3.0.0" - mdast-util-to-markdown "^1.3.0" - -mdast-util-gfm@^2.0.2: - version "2.0.2" - resolved "https://registry.yarnpkg.com/mdast-util-gfm/-/mdast-util-gfm-2.0.2.tgz#e92f4d8717d74bdba6de57ed21cc8b9552e2d0b6" - integrity sha512-qvZ608nBppZ4icQlhQQIAdc6S3Ffj9RGmzwUKUWuEICFnd1LVkN3EktF7ZHAgfcEdvZB5owU9tQgt99e2TlLjg== - dependencies: - mdast-util-from-markdown "^1.0.0" - mdast-util-gfm-autolink-literal "^1.0.0" - mdast-util-gfm-footnote "^1.0.0" - mdast-util-gfm-strikethrough "^1.0.0" - mdast-util-gfm-table "^1.0.0" - mdast-util-gfm-task-list-item "^1.0.0" - mdast-util-to-markdown "^1.0.0" - -mdast-util-phrasing@^3.0.0: - version "3.0.1" - resolved "https://registry.yarnpkg.com/mdast-util-phrasing/-/mdast-util-phrasing-3.0.1.tgz#c7c21d0d435d7fb90956038f02e8702781f95463" - integrity sha512-WmI1gTXUBJo4/ZmSk79Wcb2HcjPJBzM1nlI/OUWA8yk2X9ik3ffNbBGsU+09BFmXaL1IBb9fiuvq6/KMiNycSg== - dependencies: - "@types/mdast" "^3.0.0" - unist-util-is "^5.0.0" - -mdast-util-to-markdown@^1.0.0, mdast-util-to-markdown@^1.3.0: - version "1.5.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-markdown/-/mdast-util-to-markdown-1.5.0.tgz#c13343cb3fc98621911d33b5cd42e7d0731171c6" - integrity sha512-bbv7TPv/WC49thZPg3jXuqzuvI45IL2EVAr/KxF0BSdHsU0ceFHOmwQn6evxAh1GaoK/6GQ1wp4R4oW2+LFL/A== - dependencies: - "@types/mdast" "^3.0.0" - "@types/unist" "^2.0.0" - longest-streak "^3.0.0" - mdast-util-phrasing "^3.0.0" - mdast-util-to-string "^3.0.0" - micromark-util-decode-string "^1.0.0" - unist-util-visit "^4.0.0" - zwitch "^2.0.0" - -mdast-util-to-string@^3.0.0, mdast-util-to-string@^3.1.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/mdast-util-to-string/-/mdast-util-to-string-3.2.0.tgz#66f7bb6324756741c5f47a53557f0cbf16b6f789" - integrity sha512-V4Zn/ncyN1QNSqSBxTrMOLpjr+IKdHl2v3KVLoWmDPscP4r9GcCi71gjgvUV1SFSKh92AjAG4peFuBl2/YgCJg== - dependencies: - "@types/mdast" "^3.0.0" - media-typer@0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/media-typer/-/media-typer-0.3.0.tgz#8710d7af0aa626f8fffa1ce00168545263255748" @@ -6424,279 +6236,6 @@ methods@^1.1.2, methods@~1.1.2: resolved "https://registry.yarnpkg.com/methods/-/methods-1.1.2.tgz#5529a4d67654134edcc5266656835b0f851afcee" integrity sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w== -micromark-core-commonmark@^1.0.0, micromark-core-commonmark@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-core-commonmark/-/micromark-core-commonmark-1.1.0.tgz#1386628df59946b2d39fb2edfd10f3e8e0a75bb8" - integrity sha512-BgHO1aRbolh2hcrzL2d1La37V0Aoz73ymF8rAcKnohLy93titmv62E0gP8Hrx9PKcKrqCZ1BbLGbP3bEhoXYlw== - dependencies: - decode-named-character-reference "^1.0.0" - micromark-factory-destination "^1.0.0" - micromark-factory-label "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-factory-title "^1.0.0" - micromark-factory-whitespace "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-classify-character "^1.0.0" - micromark-util-html-tag-name "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - uvu "^0.5.0" - -micromark-extension-gfm-autolink-literal@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-1.0.5.tgz#5853f0e579bbd8ef9e39a7c0f0f27c5a063a66e7" - integrity sha512-z3wJSLrDf8kRDOh2qBtoTRD53vJ+CWIyo7uyZuxf/JAbNJjiHsOpG1y5wxk8drtv3ETAHutCu6N3thkOOgueWg== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-extension-gfm-footnote@^1.0.0: - version "1.1.2" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-1.1.2.tgz#05e13034d68f95ca53c99679040bc88a6f92fe2e" - integrity sha512-Yxn7z7SxgyGWRNa4wzf8AhYYWNrwl5q1Z8ii+CSTTIqVkmGZF1CElX2JI8g5yGoM3GAman9/PVCUFUSJ0kB/8Q== - dependencies: - micromark-core-commonmark "^1.0.0" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-gfm-strikethrough@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-1.0.7.tgz#c8212c9a616fa3bf47cb5c711da77f4fdc2f80af" - integrity sha512-sX0FawVE1o3abGk3vRjOH50L5TTLr3b5XMqnP9YDRb34M0v5OoZhG+OHFz1OffZ9dlwgpTBKaT4XW/AsUVnSDw== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-classify-character "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-gfm-table@^1.0.0: - version "1.0.7" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-table/-/micromark-extension-gfm-table-1.0.7.tgz#dcb46074b0c6254c3fc9cc1f6f5002c162968008" - integrity sha512-3ZORTHtcSnMQEKtAOsBQ9/oHp9096pI/UvdPtN7ehKvrmZZ2+bbWhi0ln+I9drmwXMt5boocn6OlwQzNXeVeqw== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-gfm-tagfilter@^1.0.0: - version "1.0.2" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-1.0.2.tgz#aa7c4dd92dabbcb80f313ebaaa8eb3dac05f13a7" - integrity sha512-5XWB9GbAUSHTn8VPU8/1DBXMuKYT5uOgEjJb8gN3mW0PNW5OPHpSdojoqf+iq1xo7vWzw/P8bAHY0n6ijpXF7g== - dependencies: - micromark-util-types "^1.0.0" - -micromark-extension-gfm-task-list-item@^1.0.0: - version "1.0.5" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-1.0.5.tgz#b52ce498dc4c69b6a9975abafc18f275b9dde9f4" - integrity sha512-RMFXl2uQ0pNQy6Lun2YBYT9g9INXtWJULgbt01D/x8/6yJ2qpKyzdZD3pi6UIkzF++Da49xAelVKUeUMqd5eIQ== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-extension-gfm@^2.0.3: - version "2.0.3" - resolved "https://registry.yarnpkg.com/micromark-extension-gfm/-/micromark-extension-gfm-2.0.3.tgz#e517e8579949a5024a493e49204e884aa74f5acf" - integrity sha512-vb9OoHqrhCmbRidQv/2+Bc6pkP0FrtlhurxZofvOEy5o8RtuuvTq+RQ1Vw5ZDNrVraQZu3HixESqbG+0iKk/MQ== - dependencies: - micromark-extension-gfm-autolink-literal "^1.0.0" - micromark-extension-gfm-footnote "^1.0.0" - micromark-extension-gfm-strikethrough "^1.0.0" - micromark-extension-gfm-table "^1.0.0" - micromark-extension-gfm-tagfilter "^1.0.0" - micromark-extension-gfm-task-list-item "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-destination@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-destination/-/micromark-factory-destination-1.1.0.tgz#eb815957d83e6d44479b3df640f010edad667b9f" - integrity sha512-XaNDROBgx9SgSChd69pjiGKbV+nfHGDPVYFs5dOoDd7ZnMAE+Cuu91BCpsY8RT2NP9vo/B8pds2VQNCLiu0zhg== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-label@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-label/-/micromark-factory-label-1.1.0.tgz#cc95d5478269085cfa2a7282b3de26eb2e2dec68" - integrity sha512-OLtyez4vZo/1NjxGhcpDSbHQ+m0IIGnT8BoPamh+7jVlzLJBH98zzuCoUeMxvM6WsNeh8wx8cKvqLiPHEACn0w== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-factory-space@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz#c8f40b0640a0150751d3345ed885a080b0d15faf" - integrity sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-title@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-title/-/micromark-factory-title-1.1.0.tgz#dd0fe951d7a0ac71bdc5ee13e5d1465ad7f50ea1" - integrity sha512-J7n9R3vMmgjDOCY8NPw55jiyaQnH5kBdV2/UXCtZIpnHH3P6nHUKaH7XXEYuWwx/xUJcawa8plLBEjMPU24HzQ== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-factory-whitespace@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-factory-whitespace/-/micromark-factory-whitespace-1.1.0.tgz#798fb7489f4c8abafa7ca77eed6b5745853c9705" - integrity sha512-v2WlmiymVSp5oMg+1Q0N1Lxmt6pMhIHD457whWM7/GUlEks1hI9xj5w3zbc4uuMKXGisksZk8DzP2UyGbGqNsQ== - dependencies: - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-character@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-character/-/micromark-util-character-1.2.0.tgz#4fedaa3646db249bc58caeb000eb3549a8ca5dcc" - integrity sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg== - dependencies: - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-chunked@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-chunked/-/micromark-util-chunked-1.1.0.tgz#37a24d33333c8c69a74ba12a14651fd9ea8a368b" - integrity sha512-Ye01HXpkZPNcV6FiyoW2fGZDUw4Yc7vT0E9Sad83+bEDiCJ1uXu0S3mr8WLpsz3HaG3x2q0HM6CTuPdcZcluFQ== - dependencies: - micromark-util-symbol "^1.0.0" - -micromark-util-classify-character@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-classify-character/-/micromark-util-classify-character-1.1.0.tgz#6a7f8c8838e8a120c8e3c4f2ae97a2bff9190e9d" - integrity sha512-SL0wLxtKSnklKSUplok1WQFoGhUdWYKggKUiqhX+Swala+BtptGCu5iPRc+xvzJ4PXE/hwM3FNXsfEVgoZsWbw== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-combine-extensions@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-combine-extensions/-/micromark-util-combine-extensions-1.1.0.tgz#192e2b3d6567660a85f735e54d8ea6e3952dbe84" - integrity sha512-Q20sp4mfNf9yEqDL50WwuWZHUrCO4fEyeDCnMGmG5Pr0Cz15Uo7KBs6jq+dq0EgX4DPwwrh9m0X+zPV1ypFvUA== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-types "^1.0.0" - -micromark-util-decode-numeric-character-reference@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-1.1.0.tgz#b1e6e17009b1f20bc652a521309c5f22c85eb1c6" - integrity sha512-m9V0ExGv0jB1OT21mrWcuf4QhP46pH1KkfWy9ZEezqHKAxkj4mPCy3nIH1rkbdMlChLHX531eOrymlwyZIf2iw== - dependencies: - micromark-util-symbol "^1.0.0" - -micromark-util-decode-string@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-decode-string/-/micromark-util-decode-string-1.1.0.tgz#dc12b078cba7a3ff690d0203f95b5d5537f2809c" - integrity sha512-YphLGCK8gM1tG1bd54azwyrQRjCFcmgj2S2GoJDNnh4vYtnL38JS8M4gpxzOPNyHdNEpheyWXCTnnTDY3N+NVQ== - dependencies: - decode-named-character-reference "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-symbol "^1.0.0" - -micromark-util-encode@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-encode/-/micromark-util-encode-1.1.0.tgz#92e4f565fd4ccb19e0dcae1afab9a173bbeb19a5" - integrity sha512-EuEzTWSTAj9PA5GOAs992GzNh2dGQO52UvAbtSOMvXTxv3Criqb6IOzJUBCmEqrrXSblJIJBbFFv6zPxpreiJw== - -micromark-util-html-tag-name@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-html-tag-name/-/micromark-util-html-tag-name-1.2.0.tgz#48fd7a25826f29d2f71479d3b4e83e94829b3588" - integrity sha512-VTQzcuQgFUD7yYztuQFKXT49KghjtETQ+Wv/zUjGSGBioZnkA4P1XXZPT1FHeJA6RwRXSF47yvJ1tsJdoxwO+Q== - -micromark-util-normalize-identifier@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-1.1.0.tgz#7a73f824eb9f10d442b4d7f120fecb9b38ebf8b7" - integrity sha512-N+w5vhqrBihhjdpM8+5Xsxy71QWqGn7HYNUvch71iV2PM7+E3uWGox1Qp90loa1ephtCxG2ftRV/Conitc6P2Q== - dependencies: - micromark-util-symbol "^1.0.0" - -micromark-util-resolve-all@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-resolve-all/-/micromark-util-resolve-all-1.1.0.tgz#4652a591ee8c8fa06714c9b54cd6c8e693671188" - integrity sha512-b/G6BTMSg+bX+xVCshPTPyAu2tmA0E4X98NSR7eIbeC6ycCqCeE7wjfDIgzEbkzdEVJXRtOG4FbEm/uGbCRouA== - dependencies: - micromark-util-types "^1.0.0" - -micromark-util-sanitize-uri@^1.0.0: - version "1.2.0" - resolved "https://registry.yarnpkg.com/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-1.2.0.tgz#613f738e4400c6eedbc53590c67b197e30d7f90d" - integrity sha512-QO4GXv0XZfWey4pYFndLUKEAktKkG5kZTdUNaTAkzbuJxn2tNBOr+QtxR2XpWaMhbImT2dPzyLrPXLlPhph34A== - dependencies: - micromark-util-character "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-symbol "^1.0.0" - -micromark-util-subtokenize@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-subtokenize/-/micromark-util-subtokenize-1.1.0.tgz#941c74f93a93eaf687b9054aeb94642b0e92edb1" - integrity sha512-kUQHyzRoxvZO2PuLzMt2P/dwVsTiivCK8icYTeR+3WgbuPqfHgPPy7nFKbeqRivBvn/3N3GBiNC+JRTMSxEC7A== - dependencies: - micromark-util-chunked "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.0" - uvu "^0.5.0" - -micromark-util-symbol@^1.0.0: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz#813cd17837bdb912d069a12ebe3a44b6f7063142" - integrity sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag== - -micromark-util-types@^1.0.0, micromark-util-types@^1.0.1: - version "1.1.0" - resolved "https://registry.yarnpkg.com/micromark-util-types/-/micromark-util-types-1.1.0.tgz#e6676a8cae0bb86a2171c498167971886cb7e283" - integrity sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg== - -micromark@^3.0.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/micromark/-/micromark-3.2.0.tgz#1af9fef3f995ea1ea4ac9c7e2f19c48fd5c006e9" - integrity sha512-uD66tJj54JLYq0De10AhWycZWGQNUvDI55xPgk2sQM5kn1JYlhbCMTtEeT27+vAhW2FBQxLlOmS3pmA7/2z4aA== - dependencies: - "@types/debug" "^4.0.0" - debug "^4.0.0" - decode-named-character-reference "^1.0.0" - micromark-core-commonmark "^1.0.1" - micromark-factory-space "^1.0.0" - micromark-util-character "^1.0.0" - micromark-util-chunked "^1.0.0" - micromark-util-combine-extensions "^1.0.0" - micromark-util-decode-numeric-character-reference "^1.0.0" - micromark-util-encode "^1.0.0" - micromark-util-normalize-identifier "^1.0.0" - micromark-util-resolve-all "^1.0.0" - micromark-util-sanitize-uri "^1.0.0" - micromark-util-subtokenize "^1.0.0" - micromark-util-symbol "^1.0.0" - micromark-util-types "^1.0.1" - uvu "^0.5.0" - micromatch@^3.1.4: version "3.1.10" resolved "https://registry.yarnpkg.com/micromatch/-/micromatch-3.1.10.tgz#70859bc95c9840952f359a068a3fc49f9ecfac23" @@ -6778,7 +6317,7 @@ minimalistic-crypto-utils@^1.0.1: resolved "https://registry.yarnpkg.com/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz#f6c00c1c0b082246e5c4d99dfb8c7c083b2b582a" integrity sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg== -minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: +minimatch@^3.0.3, minimatch@^3.0.4, minimatch@^3.0.5, minimatch@^3.1.1, minimatch@^3.1.2: version "3.1.2" resolved "https://registry.yarnpkg.com/minimatch/-/minimatch-3.1.2.tgz#19cd194bfd3e428f049a70817c038d89ab4be35b" integrity sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw== @@ -6839,7 +6378,7 @@ mkdirp-classic@^0.5.2: resolved "https://registry.yarnpkg.com/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz#fa10c9115cc6d8865be221ba47ee9bed78601113" integrity sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A== -mkdirp@1.x, mkdirp@^1.0.3: +mkdirp@1.x, mkdirp@^1.0.3, mkdirp@^1.0.4: version "1.0.4" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e" integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== @@ -6865,7 +6404,7 @@ mri@1.1.4: resolved "https://registry.yarnpkg.com/mri/-/mri-1.1.4.tgz#7cb1dd1b9b40905f1fac053abe25b6720f44744a" integrity sha512-6y7IjGPm8AzlvoUrwAaw1tLnUBudaS3752vcd8JtrpGGQn+rXIe63LFVHm/YMwtqAuh+LJPCFdlLYPWM1nYn6w== -mri@^1.1.0, mri@^1.2.0: +mri@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/mri/-/mri-1.2.0.tgz#6721480fec2a11a4889861115a48b6cbe7cc8f0b" integrity sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA== @@ -6880,7 +6419,7 @@ ms@2.1.2: resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.2.tgz#d09d1f357b443f493382a8eb3ccd183872ae6009" integrity sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w== -ms@2.1.3, ms@^2.0.0, ms@^2.1.1, ms@^2.1.3: +ms@2.1.3, ms@^2.1.1, ms@^2.1.3: version "2.1.3" resolved "https://registry.yarnpkg.com/ms/-/ms-2.1.3.tgz#574c8138ce1d2b5861f0b44579dbadd60c6615b2" integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA== @@ -7030,6 +6569,14 @@ nodemon@^2.0.19: touch "^3.1.0" undefsafe "^2.0.5" +noms@0.0.0: + version "0.0.0" + resolved "https://registry.yarnpkg.com/noms/-/noms-0.0.0.tgz#da8ebd9f3af9d6760919b27d9cdc8092a7332859" + integrity sha512-lNDU9VJaOPxUmXcLb+HQFeUgQQPtMI24Gt6hgfuMHRJgMRHMF/qZ4HJD3GDru4sSw9IQl2jPjAYnQrdIeLbwow== + dependencies: + inherits "^2.0.1" + readable-stream "~1.0.31" + nopt@~1.0.10: version "1.0.10" resolved "https://registry.yarnpkg.com/nopt/-/nopt-1.0.10.tgz#6ddd21bd2a31417b92727dd585f8a6f37608ebee" @@ -7344,6 +6891,13 @@ parse5@6.0.1: resolved "https://registry.yarnpkg.com/parse5/-/parse5-6.0.1.tgz#e1a1c085c569b3dc08321184f19a39cc27f7c30b" integrity sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw== +parse5@^7.1.2: + version "7.1.2" + resolved "https://registry.yarnpkg.com/parse5/-/parse5-7.1.2.tgz#0736bebbfd77793823240a23b7fc5e010b7f8e32" + integrity sha512-Czj1WaSVpaoj0wbhMzLmWD69anp2WH7FXMB9n1Sy8/ZFF9jolSQVMu1Ij5WIyGmcBmhk7EOndpO4mIpihVqAXw== + dependencies: + entities "^4.4.0" + parseurl@~1.3.3: version "1.3.3" resolved "https://registry.yarnpkg.com/parseurl/-/parseurl-1.3.3.tgz#9da19e7bee8d12dff0513ed5b76957793bc2e8d4" @@ -7650,6 +7204,11 @@ probot@^12.1.1, probot@^12.2.1, probot@^12.2.4: update-dotenv "^1.1.1" uuid "^8.3.2" +process-nextick-args@~2.0.0: + version "2.0.1" + resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2" + integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag== + process-warning@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/process-warning/-/process-warning-1.0.0.tgz#980a0b25dc38cd6034181be4b7726d89066b4616" @@ -7856,6 +7415,29 @@ readable-stream@^4.0.0: events "^3.3.0" process "^0.11.10" +readable-stream@~1.0.31: + version "1.0.34" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-1.0.34.tgz#125820e34bc842d2f2aaafafe4c2916ee32c157c" + integrity sha512-ok1qVCJuRkNmvebYikljxJA/UEsKwLl2nI1OmaqAu4/UE+h0wKCHok4XkL/gvi39OacXvw59RJUOFUkDib2rHg== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.1" + isarray "0.0.1" + string_decoder "~0.10.x" + +readable-stream@~2.3.6: + version "2.3.8" + resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b" + integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA== + dependencies: + core-util-is "~1.0.0" + inherits "~2.0.3" + isarray "~1.0.0" + process-nextick-args "~2.0.0" + safe-buffer "~5.1.1" + string_decoder "~1.1.1" + util-deprecate "~1.0.1" + readable-web-to-node-stream@^3.0.0: version "3.0.2" resolved "https://registry.yarnpkg.com/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz#5d52bb5df7b54861fd48d015e93a2cb87b3ee0bb" @@ -8040,14 +7622,7 @@ rxjs@^7.8.0: dependencies: tslib "^2.1.0" -sade@^1.7.3: - version "1.8.1" - resolved "https://registry.yarnpkg.com/sade/-/sade-1.8.1.tgz#0a78e81d658d394887be57d2a409bf703a3b2701" - integrity sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A== - dependencies: - mri "^1.1.0" - -safe-buffer@5.1.2: +safe-buffer@5.1.2, safe-buffer@~5.1.0, safe-buffer@~5.1.1: version "5.1.2" resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d" integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g== @@ -8507,6 +8082,18 @@ string_decoder@^1.1.1: dependencies: safe-buffer "~5.2.0" +string_decoder@~0.10.x: + version "0.10.31" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-0.10.31.tgz#62e203bc41766c6c28c9fc84301dab1c5310fa94" + integrity sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ== + +string_decoder@~1.1.1: + version "1.1.1" + resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8" + integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg== + dependencies: + safe-buffer "~5.1.0" + strip-ansi@^6.0.0, strip-ansi@^6.0.1: version "6.0.1" resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9" @@ -8710,6 +8297,14 @@ throat@^5.0.0: resolved "https://registry.yarnpkg.com/throat/-/throat-5.0.0.tgz#c5199235803aad18754a667d659b5e72ce16764b" integrity sha512-fcwX4mndzpLQKBS1DVYhGAcYaYt7vsHNIvQV+WXMvnow5cgjPphq5CaayLaGsjRdSCKZFNGt7/GYAuXaNOiYCA== +through2@^2.0.1: + version "2.0.5" + resolved "https://registry.yarnpkg.com/through2/-/through2-2.0.5.tgz#01c1e39eb31d07cb7d03a96a70823260b23132cd" + integrity sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ== + dependencies: + readable-stream "~2.3.6" + xtend "~4.0.1" + through2@^4.0.0, through2@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/through2/-/through2-4.0.2.tgz#a7ce3ac2a7a8b0b966c80e7c49f0484c3b239764" @@ -9011,37 +8606,6 @@ union-value@^1.0.0: is-extendable "^0.1.1" set-value "^2.0.1" -unist-util-is@^5.0.0: - version "5.2.1" - resolved "https://registry.yarnpkg.com/unist-util-is/-/unist-util-is-5.2.1.tgz#b74960e145c18dcb6226bc57933597f5486deae9" - integrity sha512-u9njyyfEh43npf1M+yGKDGVPbY/JWEemg5nH05ncKPfi+kBbKBJoTdsogMu33uhytuLlv9y0O7GH7fEdwLdLQw== - dependencies: - "@types/unist" "^2.0.0" - -unist-util-stringify-position@^3.0.0: - version "3.0.3" - resolved "https://registry.yarnpkg.com/unist-util-stringify-position/-/unist-util-stringify-position-3.0.3.tgz#03ad3348210c2d930772d64b489580c13a7db39d" - integrity sha512-k5GzIBZ/QatR8N5X2y+drfpWG8IDBzdnVj6OInRNWm1oXrzydiaAT2OQiA8DPRRZyAKb9b6I2a6PxYklZD0gKg== - dependencies: - "@types/unist" "^2.0.0" - -unist-util-visit-parents@^5.0.0, unist-util-visit-parents@^5.1.1: - version "5.1.3" - resolved "https://registry.yarnpkg.com/unist-util-visit-parents/-/unist-util-visit-parents-5.1.3.tgz#b4520811b0ca34285633785045df7a8d6776cfeb" - integrity sha512-x6+y8g7wWMyQhL1iZfhIPhDAs7Xwbn9nRosDXl7qoPTSCy0yNxnKc+hWokFifWQIDGi154rdUqKvbCa4+1kLhg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - -unist-util-visit@^4.0.0: - version "4.1.2" - resolved "https://registry.yarnpkg.com/unist-util-visit/-/unist-util-visit-4.1.2.tgz#125a42d1eb876283715a3cb5cceaa531828c72e2" - integrity sha512-MSd8OUGISqHdVvfY9TPhyK2VdUrPgxkUtWSuMHF6XAAFuL4LokseigBnZtPnJMu+FbynTkFNnFlyjxpVKujMRg== - dependencies: - "@types/unist" "^2.0.0" - unist-util-is "^5.0.0" - unist-util-visit-parents "^5.1.1" - universal-github-app-jwt@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/universal-github-app-jwt/-/universal-github-app-jwt-1.1.1.tgz#d57cee49020662a95ca750a057e758a1a7190e6e" @@ -9078,6 +8642,11 @@ unset-value@^1.0.0: has-value "^0.3.1" isobject "^3.0.0" +untildify@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/untildify/-/untildify-4.0.0.tgz#2bc947b953652487e4600949fb091e3ae8cd919b" + integrity sha512-KK8xQ1mkzZeg9inewmFVDNkg3l5LUhoq9kN6iWYB/CC9YMG8HA+c1Q8HwDe6dEX7kErrEVNVBO3fWsVq5iDgtw== + update-browserslist-db@^1.0.11: version "1.0.11" resolved "https://registry.yarnpkg.com/update-browserslist-db/-/update-browserslist-db-1.0.11.tgz#9a2a641ad2907ae7b3616506f4b977851db5b940" @@ -9130,7 +8699,7 @@ utif2@^4.0.1: dependencies: pako "^1.0.11" -util-deprecate@^1.0.1: +util-deprecate@^1.0.1, util-deprecate@~1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/util-deprecate/-/util-deprecate-1.0.2.tgz#450d4dc9fa70de732762fbd2d4a28981419a0ccf" integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw== @@ -9145,16 +8714,6 @@ uuid@^8.3.0, uuid@^8.3.2: resolved "https://registry.yarnpkg.com/uuid/-/uuid-8.3.2.tgz#80d5b5ced271bb9af6c445f21a1a04c606cefbe2" integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== -uvu@^0.5.0: - version "0.5.6" - resolved "https://registry.yarnpkg.com/uvu/-/uvu-0.5.6.tgz#2754ca20bcb0bb59b64e9985e84d2e81058502df" - integrity sha512-+g8ENReyr8YsOc6fv/NVJs2vFdHBnBNdfE49rshrTzDWOlUx4Gq7KOS2GD8eqhy2j+Ejq29+SbKH8yjkAqXqoA== - dependencies: - dequal "^2.0.0" - diff "^5.0.0" - kleur "^4.0.3" - sade "^1.7.3" - v8-compile-cache-lib@^3.0.1: version "3.0.1" resolved "https://registry.yarnpkg.com/v8-compile-cache-lib/-/v8-compile-cache-lib-3.0.1.tgz#6336e8d71965cb3d35a1bbb7868445a7c05264bf" @@ -9397,7 +8956,7 @@ xmlchars@^2.2.0: resolved "https://registry.yarnpkg.com/xmlchars/-/xmlchars-2.2.0.tgz#060fe1bcb7f9c76fe2a17db86a9bc3ab894210cb" integrity sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw== -xtend@^4.0.0: +xtend@^4.0.0, xtend@~4.0.1: version "4.0.2" resolved "https://registry.yarnpkg.com/xtend/-/xtend-4.0.2.tgz#bb72779f5fa465186b1f438f674fa347fdb5db54" integrity sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ== @@ -9432,7 +8991,7 @@ yaml@^2.2.2: resolved "https://registry.yarnpkg.com/yaml/-/yaml-2.3.1.tgz#02fe0975d23cd441242aa7204e09fc28ac2ac33b" integrity sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ== -yargs-parser@20.x, yargs-parser@^20.2.3: +yargs-parser@20.x, yargs-parser@^20.2.2, yargs-parser@^20.2.3: version "20.2.9" resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-20.2.9.tgz#2eb7dc3b0289718fc295f362753845c41a0c94ee" integrity sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w== @@ -9467,6 +9026,19 @@ yargs@^15.4.1: y18n "^4.0.0" yargs-parser "^18.1.2" +yargs@^16.1.0: + version "16.2.0" + resolved "https://registry.yarnpkg.com/yargs/-/yargs-16.2.0.tgz#1c82bf0f6b6a66eafce7ef30e376f49a12477f66" + integrity sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw== + dependencies: + cliui "^7.0.2" + escalade "^3.1.1" + get-caller-file "^2.0.5" + require-directory "^2.1.1" + string-width "^4.2.0" + y18n "^5.0.5" + yargs-parser "^20.2.2" + yargs@^17.0.0: version "17.7.2" resolved "https://registry.yarnpkg.com/yargs/-/yargs-17.7.2.tgz#991df39aca675a192b816e1e0363f9d75d2aa269" @@ -9497,8 +9069,3 @@ yocto-queue@^0.1.0: version "0.1.0" resolved "https://registry.yarnpkg.com/yocto-queue/-/yocto-queue-0.1.0.tgz#0294eb3dee05028d31ee1a5fa2c556a6aaf10a1b" integrity sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q== - -zwitch@^2.0.0: - version "2.0.4" - resolved "https://registry.yarnpkg.com/zwitch/-/zwitch-2.0.4.tgz#c827d4b0acb76fc3e685a4c6ec2902d51070e9d7" - integrity sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==