├── .gitattributes ├── .prettierignore ├── .vscode └── settings.json ├── .husky ├── commit-msg └── pre-commit ├── packages ├── bot │ ├── src │ │ ├── struct │ │ │ ├── GracefulTransactionError.ts │ │ │ ├── Event.ts │ │ │ ├── Component.ts │ │ │ ├── Env.ts │ │ │ ├── Command.ts │ │ │ ├── EventHandler.ts │ │ │ ├── AmaManager.ts │ │ │ ├── CommandHandler.ts │ │ │ └── SelectMenuPaginator.ts │ │ ├── util │ │ │ ├── colors.ts │ │ │ ├── NonOptionalProps.ts │ │ │ └── logger.ts │ │ ├── deploy.ts │ │ ├── events │ │ │ ├── error.ts │ │ │ ├── ready.ts │ │ │ └── interactionCreate.ts │ │ ├── index.ts │ │ ├── components │ │ │ ├── mod-deny.ts │ │ │ ├── guest-deny.ts │ │ │ ├── mod-flag.ts │ │ │ ├── guest-approve.ts │ │ │ ├── mod-approve.ts │ │ │ └── submit-question.ts │ │ └── commands │ │ │ ├── add-timestamp.ts │ │ │ ├── end.ts │ │ │ ├── add-answer.ts │ │ │ ├── start.ts │ │ │ └── ask.ts │ ├── tsconfig.json │ ├── tsconfig.eslint.json │ └── package.json └── api │ ├── not_importable.js │ ├── src │ ├── util │ │ ├── snowflakeSchema.ts │ │ ├── env.ts │ │ ├── models.ts │ │ └── logger.ts │ ├── routes │ │ ├── index.ts │ │ ├── getAMAs.ts │ │ ├── getAMA.ts │ │ └── updateAMA.ts │ ├── routeTypes.ts │ └── index.ts │ ├── tsconfig.json │ ├── tsconfig.eslint.json │ └── package.json ├── .github ├── auto_assign.yml ├── workflows │ ├── pr-automation.yml │ ├── sync-labels.yml │ ├── test.yml │ └── deploy.yml └── labels.yml ├── prisma ├── migrations │ ├── migration_lock.toml │ ├── 20231010163926_add_created_at │ │ └── migration.sql │ ├── 20231010161637_remove_stages_add_context │ │ └── migration.sql │ └── 20220810163154_init │ │ └── migration.sql └── schema.prisma ├── .dockerignore ├── .prettierrc.json ├── .gitignore ├── .commitlintrc.json ├── turbo.json ├── tsconfig.eslint.json ├── .env.example ├── .yarnrc.yml ├── .eslintrc.json ├── tsconfig.json ├── LICENSE ├── Dockerfile ├── docker-compose.yml ├── README.md ├── package.json └── .yarn └── plugins └── @yarnpkg └── plugin-workspace-tools.cjs /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | node_modules/* 2 | coverage/* 3 | **/dist/* 4 | .yarn/* 5 | .turbo 6 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn commitlint --edit $1 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | yarn build && yarn lint 5 | -------------------------------------------------------------------------------- /packages/bot/src/struct/GracefulTransactionError.ts: -------------------------------------------------------------------------------- 1 | export class GracefulTransactionFailure extends Error {} 2 | -------------------------------------------------------------------------------- /.github/auto_assign.yml: -------------------------------------------------------------------------------- 1 | addReviewers: true 2 | reviewers: 3 | - didinele 4 | numberOfReviewers: 0 5 | runOnDraft: true 6 | -------------------------------------------------------------------------------- /packages/api/not_importable.js: -------------------------------------------------------------------------------- 1 | throw new Error('This module should only be used for compile-time imports. It should not be imported in runtime.'); 2 | -------------------------------------------------------------------------------- /packages/api/src/util/snowflakeSchema.ts: -------------------------------------------------------------------------------- 1 | import { s } from '@sapphire/shapeshift'; 2 | 3 | export const snowflakeSchema = s.string.regex(/\d{17,19}/); 4 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /packages/bot/src/util/colors.ts: -------------------------------------------------------------------------------- 1 | export const Colors = { 2 | Approved: 6_931_610, 3 | Denied: 15_953_004, 4 | Flagged: 15_309_853, 5 | Blurple: 7_506_394, 6 | } as const; 7 | -------------------------------------------------------------------------------- /prisma/migrations/20231010163926_add_created_at/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Ama" ADD COLUMN "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP; 3 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | **/node_modules 2 | .env 3 | .env.*.example 4 | 5 | coverage 6 | **/dist/* 7 | **/Dockerfile 8 | docker-compose.yml 9 | 10 | .pnp.* 11 | 12 | .turbo 13 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 120, 3 | "useTabs": true, 4 | "singleQuote": true, 5 | "quoteProps": "as-needed", 6 | "trailingComma": "all", 7 | "endOfLine": "lf" 8 | } 9 | -------------------------------------------------------------------------------- /packages/api/src/routes/index.ts: -------------------------------------------------------------------------------- 1 | export { default as GetAMA } from './getAMA.js'; 2 | export { default as GetAMAs } from './getAMAs.js'; 3 | export { default as UpdateAMA } from './updateAMA.js'; 4 | -------------------------------------------------------------------------------- /packages/bot/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist" 5 | }, 6 | "include": ["./src/**/*.ts"], 7 | "exclude": ["./**/__tests__"] 8 | } 9 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | 4 | coverage/* 5 | **/dist/* 6 | 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/sdks 12 | !.yarn/versions 13 | .pnp.* 14 | 15 | .turbo 16 | logs 17 | -------------------------------------------------------------------------------- /packages/api/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "./dist", 5 | "declaration": true 6 | }, 7 | "include": ["./src/**/*.ts"], 8 | "exclude": ["./**/__tests__"] 9 | } 10 | -------------------------------------------------------------------------------- /prisma/migrations/20231010161637_remove_stages_add_context/migration.sql: -------------------------------------------------------------------------------- 1 | -- AlterTable 2 | ALTER TABLE "Ama" ALTER COLUMN "stageOnly" SET DEFAULT false; 3 | 4 | -- AlterTable 5 | ALTER TABLE "AmaQuestion" ADD COLUMN "answerMessageId" TEXT; 6 | -------------------------------------------------------------------------------- /packages/bot/src/util/NonOptionalProps.ts: -------------------------------------------------------------------------------- 1 | export type NonOptionalProps, TKeys extends keyof TObject> = { 2 | [K in keyof Omit]: TObject[K]; 3 | } & { 4 | [K in keyof Pick]-?: Exclude; 5 | }; 6 | -------------------------------------------------------------------------------- /.commitlintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["@commitlint/config-angular"], 3 | "rules": { 4 | "type-enum": [ 5 | 2, 6 | "always", 7 | ["chore", "build", "ci", "docs", "feat", "fix", "perf", "refactor", "revert", "style", "test", "types", "typings"] 8 | ], 9 | "scope-case": [0] 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "baseBranch": "origin/main", 4 | "pipeline": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "outputs": ["dist/**"] 8 | }, 9 | "lint": { 10 | "dependsOn": ["^build"], 11 | "outputs": [] 12 | } 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.github/workflows/pr-automation.yml: -------------------------------------------------------------------------------- 1 | name: 'PR Automation' 2 | on: 3 | pull_request_target: 4 | jobs: 5 | triage: 6 | runs-on: ubuntu-latest 7 | steps: 8 | - name: Automatically assign reviewers 9 | if: github.event.action == 'opened' 10 | uses: kentaro-m/auto-assign-action@v1.2.1 11 | -------------------------------------------------------------------------------- /packages/bot/src/deploy.ts: -------------------------------------------------------------------------------- 1 | import { container } from 'tsyringe'; 2 | import { CommandHandler } from './struct/CommandHandler.js'; 3 | 4 | export async function deploySlashCommands(): Promise { 5 | const commandHandler = container.resolve(CommandHandler); 6 | await commandHandler.init(); 7 | await commandHandler.registerInteractions(); 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true 5 | }, 6 | "include": [ 7 | "**/*.ts", 8 | "**/*.tsx", 9 | "**/*.js", 10 | "**/*.cjs", 11 | "**/*.jsx", 12 | "**/*.test.ts", 13 | "**/*.test.js", 14 | "**/*.spec.ts", 15 | "**/*.spec.js" 16 | ], 17 | "exclude": [] 18 | } 19 | -------------------------------------------------------------------------------- /packages/api/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true 5 | }, 6 | "include": [ 7 | "**/*.ts", 8 | "**/*.tsx", 9 | "**/*.js", 10 | "**/*.cjs", 11 | "**/*.jsx", 12 | "**/*.test.ts", 13 | "**/*.test.js", 14 | "**/*.spec.ts", 15 | "**/*.spec.js" 16 | ], 17 | "exclude": [] 18 | } 19 | -------------------------------------------------------------------------------- /packages/bot/tsconfig.eslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "compilerOptions": { 4 | "allowJs": true 5 | }, 6 | "include": [ 7 | "**/*.ts", 8 | "**/*.tsx", 9 | "**/*.js", 10 | "**/*.cjs", 11 | "**/*.jsx", 12 | "**/*.test.ts", 13 | "**/*.test.js", 14 | "**/*.spec.ts", 15 | "**/*.spec.js" 16 | ], 17 | "exclude": [] 18 | } 19 | -------------------------------------------------------------------------------- /packages/bot/src/events/error.ts: -------------------------------------------------------------------------------- 1 | import { Events } from 'discord.js'; 2 | import { singleton } from 'tsyringe'; 3 | import type { Event } from '../struct/Event'; 4 | import { logger } from '../util/logger.js'; 5 | 6 | @singleton() 7 | export default class implements Event { 8 | public readonly name = Events.Error; 9 | 10 | public handle(error: Error) { 11 | logger.error(error); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_PORT=5432 # You should only change this if you need 2 instances on the same machine 2 | DATABASE_URL="postgresql://ama:admin@localhost:${DATABASE_PORT}/ama" # You should almost never change this, unless you know what you're doing 3 | NODE_ENV=prod # Change to dev if you're trying to work on the project locally 4 | DISCORD_CLIENT_ID= # Copy this from the dev portal 5 | DISCORD_TOKEN= # Copy this from the dev portal 6 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | nodeLinker: node-modules 2 | 3 | plugins: 4 | - path: .yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs 5 | spec: '@yarnpkg/plugin-workspace-tools' 6 | - path: .yarn/plugins/@yarnpkg/plugin-version.cjs 7 | spec: '@yarnpkg/plugin-version' 8 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 9 | spec: '@yarnpkg/plugin-interactive-tools' 10 | 11 | yarnPath: .yarn/releases/yarn-3.6.4.cjs 12 | -------------------------------------------------------------------------------- /packages/bot/src/events/ready.ts: -------------------------------------------------------------------------------- 1 | import type { Client } from 'discord.js'; 2 | import { Events } from 'discord.js'; 3 | import { singleton } from 'tsyringe'; 4 | import type { Event } from '../struct/Event'; 5 | import { logger } from '../util/logger.js'; 6 | 7 | @singleton() 8 | export default class implements Event { 9 | public readonly name = Events.ClientReady; 10 | 11 | public handle(client: Client) { 12 | logger.info(`Ready as ${client.user.tag} (${client.user.id})`); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "extends": ["neon/common", "neon/node", "neon/typescript", "neon/prettier", "neon/module"], 4 | "parserOptions": { 5 | "project": "./tsconfig.eslint.json" 6 | }, 7 | "env": { 8 | "jest": true 9 | }, 10 | "globals": { 11 | "NodeJS": "readonly" 12 | }, 13 | "rules": { 14 | "curly": ["error", "all"], 15 | "eqeqeq": ["error", "always", { "null": "ignore" }], 16 | "no-eq-null": "off", 17 | "@typescript-eslint/consistent-type-definitions": ["error", "interface"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.github/workflows/sync-labels.yml: -------------------------------------------------------------------------------- 1 | name: Sync Labels 2 | on: 3 | schedule: 4 | - cron: '0 0 * * *' 5 | workflow_dispatch: 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | - '.github/labels.yml' 11 | jobs: 12 | synclabels: 13 | name: Sync Labels 14 | runs-on: ubuntu-latest 15 | steps: 16 | - name: Checkout repository 17 | uses: actions/checkout@v2 18 | 19 | - name: Sync labels 20 | uses: crazy-max/ghaction-github-labeler@v3 21 | with: 22 | github-token: ${{ secrets.GITHUB_TOKEN }} 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "strict": true, 4 | "alwaysStrict": true, 5 | "moduleResolution": "node", 6 | "removeComments": true, 7 | "pretty": true, 8 | "module": "ESNext", 9 | "target": "ES2020", 10 | "lib": ["ESNext"], 11 | "sourceMap": true, 12 | "incremental": true, 13 | "skipLibCheck": true, 14 | "noEmitHelpers": true, 15 | "importHelpers": true, 16 | "esModuleInterop": true, 17 | "noUncheckedIndexedAccess": true, 18 | "emitDecoratorMetadata": true, 19 | "experimentalDecorators": true, 20 | "noImplicitOverride": true, 21 | "verbatimModuleSyntax": true 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/bot/src/struct/Event.ts: -------------------------------------------------------------------------------- 1 | import { basename, extname } from 'node:path'; 2 | import type { ClientEvents } from 'discord.js'; 3 | 4 | export interface Event { 5 | handle(...args: ClientEvents[Name]): unknown; 6 | readonly name?: Name; 7 | } 8 | 9 | export type EventConstructor = new (...args: any[]) => Event; 10 | 11 | export interface EventInfo { 12 | name: string; 13 | } 14 | 15 | export function getEventInfo(path: string): EventInfo | null { 16 | if (extname(path) !== '.js') { 17 | return null; 18 | } 19 | 20 | return { name: basename(path, '.js') }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/api/src/util/env.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import { singleton } from 'tsyringe'; 3 | 4 | @singleton() 5 | export class Env { 6 | public readonly port: number = Number.parseInt(process.env.PORT ?? '8080', 10); 7 | 8 | public readonly isProd = process.env.NODE_ENV === 'prod'; 9 | 10 | public readonly cors = process.env.CORS?.split(',') ?? []; 11 | 12 | private readonly KEYS: string[] = []; 13 | 14 | public constructor() { 15 | for (const key of this.KEYS) { 16 | if (!(key in process.env)) { 17 | throw new Error(`Missing required environment variable: ${key}`); 18 | } 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2023 ChatSift 2 | 3 | This program is free software: you can redistribute it and/or modify 4 | it under the terms of the GNU Affero General Public License as 5 | published by the Free Software Foundation, either version 3 of the 6 | License, or (at your option) any later version. 7 | 8 | This program is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 | GNU Affero General Public License for more details. 12 | 13 | You should have received a copy of the GNU Affero General Public License 14 | along with this program. If not, see . 15 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-alpine 2 | LABEL name "ama" 3 | 4 | WORKDIR /usr/ama 5 | 6 | RUN apk add --update \ 7 | && apk add --no-cache ca-certificates \ 8 | && apk add --no-cache --virtual .build-deps curl git python3 alpine-sdk 9 | 10 | COPY turbo.json package.json tsconfig.json yarn.lock .yarnrc.yml ./ 11 | COPY .yarn ./.yarn 12 | 13 | COPY packages/api/package.json ./packages/api/package.json 14 | COPY packages/bot/package.json ./packages/bot/package.json 15 | 16 | RUN yarn --immutable 17 | 18 | COPY prisma ./prisma 19 | RUN yarn prisma generate 20 | 21 | COPY packages/api ./packages/api 22 | COPY packages/bot ./packages/bot 23 | 24 | RUN yarn turbo run build 25 | 26 | RUN yarn workspaces focus --all --production 27 | -------------------------------------------------------------------------------- /packages/bot/src/struct/Component.ts: -------------------------------------------------------------------------------- 1 | import { basename, extname } from 'node:path'; 2 | import type { Awaitable, MessageComponentInteraction } from 'discord.js'; 3 | 4 | export interface ComponentInfo { 5 | name: string; 6 | } 7 | 8 | export interface Component = MessageComponentInteraction<'cached'>> { 9 | handle(interaction: Type, ...args: any[]): Awaitable; 10 | readonly name?: string; 11 | } 12 | 13 | export type ComponentConstructor = new (...args: any[]) => Component; 14 | 15 | export function getComponentInfo(path: string): ComponentInfo | null { 16 | if (extname(path) !== '.js') { 17 | return null; 18 | } 19 | 20 | return { name: basename(path, '.js') }; 21 | } 22 | -------------------------------------------------------------------------------- /packages/bot/src/struct/Env.ts: -------------------------------------------------------------------------------- 1 | import process from 'node:process'; 2 | import { singleton } from 'tsyringe'; 3 | 4 | @singleton() 5 | export class Env { 6 | public readonly discordToken = process.env.DISCORD_TOKEN!; 7 | 8 | public readonly discordClientId = process.env.DISCORD_CLIENT_ID!; 9 | 10 | public readonly isProd = process.env.NODE_ENV === 'prod'; 11 | 12 | public readonly deploySlashCommands = Boolean(process.env.DEPLOY); 13 | 14 | public readonly debugJobs = process.env.DEBUG_JOBS === 'true'; 15 | 16 | private readonly KEYS = ['DISCORD_TOKEN', 'DISCORD_CLIENT_ID', 'NODE_ENV'] as const; 17 | 18 | public constructor() { 19 | for (const key of this.KEYS) { 20 | if (!(key in process.env)) { 21 | throw new Error(`Missing required environment variable: ${key}`); 22 | } 23 | } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: Quality Check 2 | on: [push, pull_request] 3 | jobs: 4 | quality: 5 | name: Quality Check 6 | runs-on: ubuntu-latest 7 | env: 8 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 9 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 10 | steps: 11 | - name: Checkout repository 12 | uses: actions/checkout@v2 13 | 14 | - name: Install Node.js 15 | uses: actions/setup-node@v2 16 | with: 17 | node-version: 20 18 | cache: 'yarn' 19 | cache-dependency-path: yarn.lock 20 | 21 | - name: Install dependencies 22 | run: yarn --immutable 23 | 24 | - name: Ensure prisma schema is up to date 25 | run: yarn prisma generate 26 | 27 | - name: Build 28 | run: yarn build 29 | 30 | - name: ESLint 31 | run: yarn lint 32 | -------------------------------------------------------------------------------- /packages/api/src/routes/getAMAs.ts: -------------------------------------------------------------------------------- 1 | import { Route, RouteMethod } from '@chatsift/rest-utils'; 2 | import { PrismaClient } from '@prisma/client'; 3 | import type { Request, Response } from 'polka'; 4 | import { singleton } from 'tsyringe'; 5 | import type { Ama } from '../util/models'; 6 | 7 | @singleton() 8 | export default class extends Route { 9 | public info = { 10 | method: RouteMethod.get, 11 | path: '/ama/v1/guilds/:guildId/amas/', 12 | } as const; 13 | 14 | public constructor(private readonly prisma: PrismaClient) { 15 | super(); 16 | } 17 | 18 | public async handle(req: Request, res: Response) { 19 | const { guildId } = req.params as { guildId: string }; 20 | const amas = await this.prisma.ama.findMany({ 21 | where: { 22 | guildId, 23 | }, 24 | }); 25 | 26 | res.statusCode = 200; 27 | res.setHeader('Content-Type', 'application/json'); 28 | res.end(JSON.stringify(amas)); 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/api/src/util/models.ts: -------------------------------------------------------------------------------- 1 | // !!! PLEASE READ !!! 2 | // This file's content is snatched straight out of our generated @prisma/client 3 | // It's here because we need it for Routes to use types that DON'T rely on prisma 4 | // Because otherwise we would need to somehow share our prisma.schema (and 2 others) with the frontend 5 | // Which would NOT work. Absolutely make sure to use the types below and to cast away any types from @prsisma/client 6 | 7 | export interface Ama { 8 | answersChannel: string; 9 | ended: boolean; 10 | flaggedQueue: string | null; 11 | guestQueue: string | null; 12 | guildId: string; 13 | id: number; 14 | modQueue: string | null; 15 | promptChannelId: string; 16 | promptMessageId: string; 17 | stageOnly: boolean; 18 | title: string; 19 | } 20 | 21 | export interface AmaQuestion { 22 | amaId: number; 23 | authorId: string; 24 | content: string; 25 | id: number; 26 | imageUrl: string | null; 27 | } 28 | -------------------------------------------------------------------------------- /.github/workflows/deploy.yml: -------------------------------------------------------------------------------- 1 | name: Deploy 2 | 3 | on: 4 | workflow_run: 5 | workflows: 6 | - 'Quality Check' 7 | branches: 8 | - main 9 | types: 10 | - completed 11 | workflow_dispatch: 12 | 13 | jobs: 14 | deploy: 15 | name: Deploy 16 | runs-on: ubuntu-latest 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v2 21 | 22 | - name: Install Node 23 | uses: actions/setup-node@v1 24 | with: 25 | node-version: 20 26 | 27 | - name: Set up Docker Buildx 28 | uses: docker/setup-buildx-action@v1 29 | 30 | - name: Login to DockerHub 31 | run: docker login -u ${{ secrets.DOCKERHUB_USERNAME }} -p ${{ secrets.DOCKERHUB_TOKEN }} 32 | 33 | - name: Build the images 34 | run: docker build -t chatsift/ama:latest -f ./Dockerfile . 35 | 36 | - name: Push to DockerHub 37 | run: docker push --all-tags chatsift/ama 38 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "postgresql" 7 | url = env("DATABASE_URL") 8 | } 9 | 10 | model Ama { 11 | id Int @id @default(autoincrement()) 12 | guildId String 13 | modQueue String? 14 | flaggedQueue String? 15 | guestQueue String? 16 | title String 17 | answersChannel String 18 | promptChannelId String 19 | promptMessageId String @unique 20 | stageOnly Boolean @default(false) // deprecated, hence the defualt. 21 | ended Boolean @default(false) 22 | createdAt DateTime @default(now()) 23 | 24 | questions AmaQuestion[] 25 | } 26 | 27 | model AmaQuestion { 28 | id Int @id @default(autoincrement()) 29 | amaId Int 30 | ama Ama @relation(fields: [amaId], references: [id], onDelete: Cascade) 31 | authorId String 32 | content String 33 | imageUrl String? 34 | answerMessageId String? 35 | } 36 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3.7' 2 | 3 | services: 4 | postgres: 5 | image: postgres:12-alpine 6 | environment: 7 | POSTGRES_USER: 'ama' 8 | POSTGRES_PASSWORD: 'admin' 9 | POSTGRES_DB: 'ama' 10 | volumes: 11 | - postgres-data:/var/lib/postgresql/data 12 | restart: unless-stopped 13 | ports: 14 | - 127.0.0.1:${DATABASE_PORT}:5432 15 | healthcheck: 16 | test: ['CMD-SHELL', 'pg_isready -U ama'] 17 | interval: 10s 18 | timeout: 5s 19 | 20 | bot: 21 | image: chatsift/ama 22 | build: 23 | context: ./ 24 | dockerfile: ./Dockerfile 25 | env_file: 26 | - ./.env 27 | environment: 28 | DATABASE_URL: 'postgresql://ama:admin@postgres:5432/ama' 29 | restart: unless-stopped 30 | volumes: 31 | - ./logs:/usr/ama/logs 32 | depends_on: 33 | - postgres 34 | command: ['node', '--enable-source-maps', '--no-warnings', './packages/bot/dist/index.js'] 35 | 36 | volumes: 37 | postgres-data: 38 | name: 'ama-postgres-data' 39 | -------------------------------------------------------------------------------- /packages/api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatsift/ama-api", 3 | "main": "./not_importable.js", 4 | "types": "./dist/index.d.ts", 5 | "version": "0.1.1", 6 | "type": "module", 7 | "files": [ 8 | "dist" 9 | ], 10 | "scripts": { 11 | "lint": "eslint src --ext .ts", 12 | "build": "tsc" 13 | }, 14 | "engines": { 15 | "node": ">=16.9.0" 16 | }, 17 | "devDependencies": { 18 | "@types/cors": "^2.8.14", 19 | "@types/node": "^20.8.4", 20 | "prisma": "^5.4.2", 21 | "typescript": "^5.2.2" 22 | }, 23 | "dependencies": { 24 | "@chatsift/pino-rotate-file": "^0.2.0", 25 | "@chatsift/readdir": "^0.3.0", 26 | "@chatsift/rest-utils": "^0.7.0", 27 | "@hapi/boom": "^10.0.1", 28 | "@prisma/client": "^5.4.2", 29 | "@sapphire/shapeshift": "^3.9.2", 30 | "cors": "^2.8.5", 31 | "helmet": "^7.0.0", 32 | "pino": "^8.16.0", 33 | "pino-pretty": "^10.2.3", 34 | "polka": "^1.0.0-next.22", 35 | "prisma-error-enum": "^0.1.3", 36 | "reflect-metadata": "^0.1.13", 37 | "tslib": "^2.6.2", 38 | "tsyringe": "^4.8.0" 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /packages/bot/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatsift/ama-bot", 3 | "main": "./dist/index.js", 4 | "private": true, 5 | "version": "2.0.0", 6 | "type": "module", 7 | "imports": { 8 | "#struct/*": "./dist/struct/*.js", 9 | "#util/*": "./dist/util/*.js" 10 | }, 11 | "scripts": { 12 | "lint": "eslint src --ext .ts", 13 | "build": "tsc" 14 | }, 15 | "engines": { 16 | "node": ">=16.9.0" 17 | }, 18 | "devDependencies": { 19 | "@types/common-tags": "^1.8.2", 20 | "@types/node": "^20.8.4", 21 | "prisma": "^5.4.2", 22 | "typescript": "^5.2.2" 23 | }, 24 | "dependencies": { 25 | "@chatsift/pino-rotate-file": "^0.2.0", 26 | "@chatsift/readdir": "^0.3.0", 27 | "@discordjs/rest": "^2.0.1", 28 | "@naval-base/ms": "^3.1.0", 29 | "@prisma/client": "^5.4.2", 30 | "@sapphire/result": "^2.6.4", 31 | "common-tags": "^1.8.2", 32 | "discord.js": "^14.13.0", 33 | "nanoid": "^5.0.1", 34 | "pino": "^8.16.0", 35 | "pino-pretty": "^10.2.3", 36 | "prisma-error-enum": "^0.1.3", 37 | "reflect-metadata": "^0.1.13", 38 | "tslib": "^2.6.2", 39 | "tsyringe": "^4.8.0" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /prisma/migrations/20220810163154_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "Ama" ( 3 | "id" SERIAL NOT NULL, 4 | "guildId" TEXT NOT NULL, 5 | "modQueue" TEXT, 6 | "flaggedQueue" TEXT, 7 | "guestQueue" TEXT, 8 | "title" TEXT NOT NULL, 9 | "answersChannel" TEXT NOT NULL, 10 | "promptChannelId" TEXT NOT NULL, 11 | "promptMessageId" TEXT NOT NULL, 12 | "stageOnly" BOOLEAN NOT NULL, 13 | "ended" BOOLEAN NOT NULL DEFAULT false, 14 | 15 | CONSTRAINT "Ama_pkey" PRIMARY KEY ("id") 16 | ); 17 | 18 | -- CreateTable 19 | CREATE TABLE "AmaQuestion" ( 20 | "id" SERIAL NOT NULL, 21 | "amaId" INTEGER NOT NULL, 22 | "authorId" TEXT NOT NULL, 23 | "content" TEXT NOT NULL, 24 | "imageUrl" TEXT, 25 | 26 | CONSTRAINT "AmaQuestion_pkey" PRIMARY KEY ("id") 27 | ); 28 | 29 | -- CreateIndex 30 | CREATE UNIQUE INDEX "Ama_promptMessageId_key" ON "Ama"("promptMessageId"); 31 | 32 | -- AddForeignKey 33 | ALTER TABLE "AmaQuestion" ADD CONSTRAINT "AmaQuestion_amaId_fkey" FOREIGN KEY ("amaId") REFERENCES "Ama"("id") ON DELETE CASCADE ON UPDATE CASCADE; 34 | -------------------------------------------------------------------------------- /packages/bot/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import process from 'node:process'; 3 | import { PrismaClient } from '@prisma/client'; 4 | import { Client, IntentsBitField, Options, Partials } from 'discord.js'; 5 | import { container } from 'tsyringe'; 6 | import { deploySlashCommands } from './deploy.js'; 7 | import { CommandHandler } from './struct/CommandHandler.js'; 8 | import { Env } from './struct/Env.js'; 9 | import { EventHandler } from './struct/EventHandler.js'; 10 | 11 | const env = container.resolve(Env); 12 | 13 | const client = new Client({ 14 | intents: [IntentsBitField.Flags.Guilds], 15 | partials: [Partials.Channel, Partials.Message], 16 | makeCache: Options.cacheWithLimits({ MessageManager: 100 }), 17 | }).setMaxListeners(20); 18 | container.register(Client, { useValue: client }); 19 | container.register(PrismaClient, { useValue: new PrismaClient() }); 20 | 21 | if (env.deploySlashCommands) { 22 | await deploySlashCommands(); 23 | process.exit(0); 24 | } 25 | 26 | await container.resolve(CommandHandler).init(); 27 | await container.resolve(EventHandler).init(); 28 | 29 | await client.login(env.discordToken); 30 | -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | - name: 'backlog' 2 | color: '7ef7ef' 3 | - name: 'bug' 4 | color: 'd73a4a' 5 | - name: 'chore' 6 | color: 'ffffff' 7 | - name: 'ci' 8 | color: '0075ca' 9 | - name: 'dependencies' 10 | color: '276bd1' 11 | - name: 'documentation' 12 | color: '0075ca' 13 | - name: 'duplicate' 14 | color: 'cfd3d7' 15 | - name: 'feature request' 16 | color: 'fcf95a' 17 | - name: 'good first issue' 18 | color: '7057ff' 19 | - name: 'has PR' 20 | color: '4b1f8e' 21 | - name: 'help wanted' 22 | color: '008672' 23 | - name: 'in progress' 24 | color: 'ffccd7' 25 | - name: 'in review' 26 | color: 'aed5fc' 27 | - name: 'invalid' 28 | color: 'e4e669' 29 | - name: 'need repro' 30 | color: 'c66037' 31 | - name: 'performance' 32 | color: '80c042' 33 | - name: 'priority:high' 34 | color: 'fc1423' 35 | - name: 'refactor' 36 | color: '1d637f' 37 | - name: 'regression' 38 | color: 'ea8785' 39 | - name: 'semver:major' 40 | color: 'c10f47' 41 | - name: 'semver:minor' 42 | color: 'e4f486' 43 | - name: 'semver:patch' 44 | color: 'e8be8b' 45 | - name: 'tests' 46 | color: 'f06dff' 47 | - name: 'wontfix' 48 | color: 'ffffff' 49 | -------------------------------------------------------------------------------- /packages/bot/src/struct/Command.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | ApplicationCommandOptionChoiceData, 3 | ApplicationCommandType, 4 | AutocompleteInteraction, 5 | Awaitable, 6 | ChatInputCommandInteraction, 7 | MessageContextMenuCommandInteraction, 8 | RESTPostAPIApplicationCommandsJSONBody, 9 | UserContextMenuCommandInteraction, 10 | } from 'discord.js'; 11 | 12 | interface InteractionTypeMapping { 13 | [ApplicationCommandType.ChatInput]: ChatInputCommandInteraction<'cached'>; 14 | [ApplicationCommandType.User]: UserContextMenuCommandInteraction<'cached'>; 15 | [ApplicationCommandType.Message]: MessageContextMenuCommandInteraction<'cached'>; 16 | } 17 | 18 | export type CommandBody = RESTPostAPIApplicationCommandsJSONBody & { 19 | type: Type; 20 | }; 21 | 22 | export interface Command { 23 | handle(interaction: InteractionTypeMapping[Type]): Awaitable; 24 | handleAutocomplete?(interaction: AutocompleteInteraction): Awaitable; 25 | readonly interactionOptions: CommandBody; 26 | } 27 | 28 | export type CommandConstructor = new (...args: any[]) => Command; 29 | -------------------------------------------------------------------------------- /packages/bot/src/struct/EventHandler.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from 'node:path'; 2 | import { fileURLToPath, pathToFileURL } from 'node:url'; 3 | import { readdirRecurse } from '@chatsift/readdir'; 4 | import type { ClientEvents } from 'discord.js'; 5 | import { Client } from 'discord.js'; 6 | import { container, singleton } from 'tsyringe'; 7 | import { getEventInfo, type EventConstructor } from './Event.js'; 8 | 9 | @singleton() 10 | export class EventHandler { 11 | public constructor(private readonly client: Client) {} 12 | 13 | public async init(): Promise { 14 | const path = join(dirname(fileURLToPath(import.meta.url)), '..', 'events'); 15 | const files = readdirRecurse(path, { fileExtensions: ['js'] }); 16 | 17 | for await (const file of files) { 18 | const info = getEventInfo(file); 19 | if (!info) { 20 | continue; 21 | } 22 | 23 | const mod = (await import(pathToFileURL(file).toString())) as { default: EventConstructor }; 24 | const event = container.resolve(mod.default); 25 | const name = event.name ?? (info.name as keyof ClientEvents); 26 | 27 | // @ts-expect-error - TS doesn't deal with unions here as I'd expect it to 28 | this.client.on(name, (...data) => event.handle(...data)); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/api/src/util/logger.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path'; 2 | import process from 'node:process'; 3 | import type { PinoRotateFileOptions } from '@chatsift/pino-rotate-file'; 4 | import createLogger, { multistream, transport } from 'pino'; 5 | import type { PrettyOptions } from 'pino-pretty'; 6 | 7 | const pinoPrettyOptions: PrettyOptions = { 8 | colorize: true, 9 | levelFirst: true, 10 | translateTime: true, 11 | }; 12 | 13 | const pinoRotateFileOptions: PinoRotateFileOptions = { 14 | dir: join(process.cwd(), 'logs', 'api'), 15 | mkdir: true, 16 | maxAgeDays: 14, 17 | prettyOptions: { 18 | ...pinoPrettyOptions, 19 | colorize: false, 20 | }, 21 | }; 22 | 23 | export const logger = createLogger( 24 | { 25 | name: 'API', 26 | level: 'trace', 27 | }, 28 | multistream([ 29 | { 30 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 31 | stream: transport({ 32 | target: 'pino-pretty', 33 | options: pinoPrettyOptions, 34 | }), 35 | level: 'trace', 36 | }, 37 | { 38 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 39 | stream: transport({ 40 | target: '@chatsift/pino-rotate-file', 41 | options: pinoRotateFileOptions, 42 | }), 43 | level: 'trace', 44 | }, 45 | ]), 46 | ); 47 | -------------------------------------------------------------------------------- /packages/bot/src/util/logger.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'node:path'; 2 | import process from 'node:process'; 3 | import type { PinoRotateFileOptions } from '@chatsift/pino-rotate-file'; 4 | import createLogger, { multistream, transport } from 'pino'; 5 | import type { PrettyOptions } from 'pino-pretty'; 6 | 7 | const pinoPrettyOptions: PrettyOptions = { 8 | colorize: true, 9 | levelFirst: true, 10 | translateTime: true, 11 | }; 12 | 13 | const pinoRotateFileOptions: PinoRotateFileOptions = { 14 | dir: join(process.cwd(), 'logs', 'bot'), 15 | mkdir: true, 16 | maxAgeDays: 14, 17 | prettyOptions: { 18 | ...pinoPrettyOptions, 19 | colorize: false, 20 | }, 21 | }; 22 | 23 | export const logger = createLogger( 24 | { 25 | name: 'BOT', 26 | level: 'trace', 27 | }, 28 | multistream([ 29 | { 30 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 31 | stream: transport({ 32 | target: 'pino-pretty', 33 | options: pinoPrettyOptions, 34 | }), 35 | level: 'trace', 36 | }, 37 | { 38 | // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment 39 | stream: transport({ 40 | target: '@chatsift/pino-rotate-file', 41 | options: pinoRotateFileOptions, 42 | }), 43 | level: 'trace', 44 | }, 45 | ]), 46 | ); 47 | -------------------------------------------------------------------------------- /packages/api/src/routes/getAMA.ts: -------------------------------------------------------------------------------- 1 | import { Route, RouteMethod } from '@chatsift/rest-utils'; 2 | import { badRequest, notFound } from '@hapi/boom'; 3 | import { PrismaClient } from '@prisma/client'; 4 | import type { NextHandler, Request, Response } from 'polka'; 5 | import { singleton } from 'tsyringe'; 6 | import type { Ama } from '../util/models'; 7 | 8 | @singleton() 9 | export default class extends Route { 10 | public info = { 11 | method: RouteMethod.get, 12 | path: '/ama/v1/guilds/:guildId/amas/:amaId', 13 | } as const; 14 | 15 | public constructor(private readonly prisma: PrismaClient) { 16 | super(); 17 | } 18 | 19 | public async handle(req: Request, res: Response, next: NextHandler) { 20 | const { guildId, amaId } = req.params as { amaId: string; guildId: string }; 21 | 22 | const amaIdNum = Number.parseInt(amaId, 10); 23 | if (Number.isNaN(amaIdNum)) { 24 | return next(badRequest('Invalid AMA ID')); 25 | } 26 | 27 | const ama = await this.prisma.ama.findFirst({ 28 | where: { 29 | guildId, 30 | id: amaIdNum, 31 | }, 32 | }); 33 | if (!ama) { 34 | return next(notFound('AMA not found')); 35 | } 36 | 37 | res.statusCode = 200; 38 | res.setHeader('Content-Type', 'application/json'); 39 | res.end(JSON.stringify(ama)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /packages/bot/src/components/mod-deny.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | import type { ButtonInteraction } from 'discord.js'; 3 | import { singleton } from 'tsyringe'; 4 | import type { Component } from '../struct/Component'; 5 | import { Colors } from '../util/colors.js'; 6 | 7 | @singleton() 8 | export default class implements Component> { 9 | public constructor(private readonly prisma: PrismaClient) {} 10 | 11 | public async handle(interaction: ButtonInteraction<'cached'>, rawQuestionId: string) { 12 | const questionId = Number.parseInt(rawQuestionId, 10); 13 | const question = await this.prisma.amaQuestion.findFirst({ 14 | where: { id: questionId }, 15 | include: { ama: true }, 16 | }); 17 | 18 | if (!question) { 19 | await interaction.reply({ 20 | content: 'No AMA found, this is likely a bug.', 21 | ephemeral: true, 22 | }); 23 | return; 24 | } 25 | 26 | if (question.ama.ended) { 27 | await interaction.reply({ 28 | content: 'This AMA has already ended.', 29 | ephemeral: true, 30 | }); 31 | return; 32 | } 33 | 34 | await interaction.update({ 35 | embeds: [ 36 | { 37 | ...interaction.message.embeds[0]?.toJSON(), 38 | color: Colors.Denied, 39 | }, 40 | ], 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/bot/src/components/guest-deny.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | import type { ButtonInteraction } from 'discord.js'; 3 | import { singleton } from 'tsyringe'; 4 | import type { Component } from '../struct/Component.js'; 5 | import { Colors } from '../util/colors.js'; 6 | 7 | @singleton() 8 | export default class implements Component> { 9 | public constructor(private readonly prisma: PrismaClient) {} 10 | 11 | public async handle(interaction: ButtonInteraction<'cached'>, rawQuestionId: string) { 12 | const questionId = Number.parseInt(rawQuestionId, 10); 13 | const question = await this.prisma.amaQuestion.findFirst({ 14 | where: { id: questionId }, 15 | include: { ama: true }, 16 | }); 17 | 18 | if (!question) { 19 | await interaction.reply({ 20 | content: 'No AMA found, this is likely a bug.', 21 | ephemeral: true, 22 | }); 23 | return; 24 | } 25 | 26 | if (question.ama.ended) { 27 | await interaction.reply({ 28 | content: 'This AMA has already ended.', 29 | ephemeral: true, 30 | }); 31 | return; 32 | } 33 | 34 | await interaction.update({ 35 | embeds: [ 36 | { 37 | ...interaction.message.embeds[0]?.toJSON(), 38 | color: Colors.Denied, 39 | }, 40 | ], 41 | }); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /packages/api/src/routeTypes.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | InferRoutePath, 3 | InferRouteMethod, 4 | InferRouteBody, 5 | InferRouteResult, 6 | RouteMethod, 7 | } from '@chatsift/rest-utils'; 8 | import type * as routes from './routes/index'; 9 | 10 | type Narrow = T extends U ? T : never; 11 | type ConstructorToType = TConstructor extends new (...args: any[]) => infer T ? T : never; 12 | type RoutesByClassNames = { 13 | [K in keyof typeof routes]: ConstructorToType<(typeof routes)[K]>; 14 | }; 15 | type RoutesByPaths = { 16 | [Path in InferRoutePath]: Narrow< 17 | RoutesByClassNames[keyof RoutesByClassNames], 18 | { info: { path: Path } } 19 | >; 20 | }; 21 | 22 | interface RouteMethodMap { 23 | [RouteMethod.get]: 'get'; 24 | [RouteMethod.post]: 'post'; 25 | [RouteMethod.put]: 'put'; 26 | [RouteMethod.delete]: 'delete'; 27 | [RouteMethod.patch]: 'patch'; 28 | } 29 | 30 | export type AMARoutes = { 31 | [Path in keyof RoutesByPaths]: { 32 | [Method in RouteMethodMap[InferRouteMethod]]: Narrow< 33 | RoutesByPaths[Path], 34 | { info: { method: Method } } 35 | >; 36 | }; 37 | }; 38 | 39 | export type InferAMARouteBody = InferRouteBody< 40 | AMARoutes[TPath][TMethod] 41 | >; 42 | 43 | export type InferAMARouteResult< 44 | TPath extends keyof AMARoutes, 45 | TMethod extends keyof AMARoutes[TPath], 46 | > = InferRouteResult; 47 | 48 | export * from './util/models.js'; 49 | -------------------------------------------------------------------------------- /packages/bot/src/events/interactionCreate.ts: -------------------------------------------------------------------------------- 1 | import type { Interaction } from 'discord.js'; 2 | import { Events, InteractionType } from 'discord.js'; 3 | import { singleton } from 'tsyringe'; 4 | import { CommandHandler } from '../struct/CommandHandler.js'; 5 | import type { Event } from '../struct/Event'; 6 | import { logger } from '../util/logger.js'; 7 | 8 | @singleton() 9 | export default class implements Event { 10 | public readonly name = Events.InteractionCreate; 11 | 12 | public constructor(private readonly commandHandler: CommandHandler) {} 13 | 14 | public async handle(interaction: Interaction) { 15 | switch (interaction.type) { 16 | case InteractionType.ApplicationCommandAutocomplete: { 17 | await this.commandHandler.handleAutocomplete(interaction); 18 | break; 19 | } 20 | 21 | case InteractionType.MessageComponent: { 22 | if (interaction.inCachedGuild()) { 23 | await this.commandHandler.handleMessageComponent(interaction); 24 | } 25 | 26 | break; 27 | } 28 | 29 | case InteractionType.ApplicationCommand: { 30 | await this.commandHandler.handleCommand(interaction); 31 | break; 32 | } 33 | 34 | case InteractionType.ModalSubmit: { 35 | break; 36 | } 37 | 38 | default: { 39 | // Cast to any to avoid TS error - we get one since this default case technically handles nothing right now, 40 | // but would if Discord added a new interaction type 41 | logger.warn(`Unknown interaction type: ${(interaction as any).type}`); 42 | break; 43 | } 44 | } 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # AMA 2 | 3 | ## About 4 | 5 | You can read all about the bot [here](todo). 6 | 7 | ## Self hosting 8 | 9 | This repository contains source code for the bot itself under [packages/bot](./packages/bot/). 10 | A Docker image that can be used for running anything in this monorepo is available on DockerHub under `chatsift/ama`. 11 | 12 | --- 13 | 14 | With all those notices out of the way, the [docker-compose.yml](./docker-compose.yml) file 15 | is probably the easiest way to get started. 16 | 17 | Before you do anything else (even if you're using Docker), make sure to run `yarn --immutable`. 18 | If you don't have yarn installed, `npm i -g yarn` (assuming you have a nodejs installation). 19 | 20 | Simply create a new file called `.env`, follow the example from [.env.example](./.env.example), 21 | and then `docker-compose build && docker-compose up -d`. 22 | 23 | Now that the bot and postgres server are up, run `yarn deploy-commands` to register 24 | the global slash commands, and then `yarn prisma migrate deploy` to get the database ready. 25 | 26 | Alternatively, you can run your own postgresql instance, build the code with `yarn build`, 27 | and start up the bot using `yarn start-bot` 28 | in whatever way keeps it online (e.g. pm2). 29 | 30 | --- 31 | 32 | ## Updating a self-hosted instance 33 | 34 | Assuming you're using Docker, you essentially just need to follow the steps above again. 35 | `docker-compose build && docker-compose up -d`, re-deploy slash commands, and deploy prisma 36 | migrations. 37 | 38 | ## Contributing/working on the project 39 | 40 | Just about everything above, except set the `NODE_ENV` env var to `dev`. If you're trying to 41 | figure out something wrong with cron jobs, `DEBUG_JOBS=true`. 42 | 43 | ## Licensing 44 | 45 | This project is lincensed under the GNU AGPLv3 license. View the full file [here](./LICENSE). 46 | -------------------------------------------------------------------------------- /packages/api/src/routes/updateAMA.ts: -------------------------------------------------------------------------------- 1 | import type { TRequest } from '@chatsift/rest-utils'; 2 | import { Route, RouteMethod } from '@chatsift/rest-utils'; 3 | import { badRequest, notFound } from '@hapi/boom'; 4 | import { PrismaClient } from '@prisma/client'; 5 | import type { BaseValidator, InferType } from '@sapphire/shapeshift'; 6 | import { s } from '@sapphire/shapeshift'; 7 | import type { NextHandler, Response } from 'polka'; 8 | import { singleton } from 'tsyringe'; 9 | import type { Ama } from '../util/models'; 10 | 11 | const schema = s.object({ 12 | ended: s.boolean, 13 | }).strict; 14 | type Body = InferType; 15 | 16 | @singleton() 17 | export default class extends Route { 18 | public info = { 19 | method: RouteMethod.patch, 20 | path: '/ama/v1/guilds/:guildId/amas/:amaId', 21 | } as const; 22 | 23 | public override readonly bodyValidationSchema: BaseValidator = schema; 24 | 25 | public constructor(private readonly prisma: PrismaClient) { 26 | super(); 27 | } 28 | 29 | public async handle(req: TRequest, res: Response, next: NextHandler) { 30 | const { guildId, amaId } = req.params as { amaId: string; guildId: string }; 31 | 32 | const amaIdNum = Number.parseInt(amaId, 10); 33 | if (Number.isNaN(amaIdNum)) { 34 | return next(badRequest('Invalid AMA ID')); 35 | } 36 | 37 | const ama = await this.prisma.ama.findFirst({ 38 | where: { 39 | guildId, 40 | id: amaIdNum, 41 | }, 42 | }); 43 | if (!ama) { 44 | return next(notFound('AMA not found')); 45 | } 46 | 47 | const updatedAMA = await this.prisma.ama.update({ 48 | where: { 49 | id: amaIdNum, 50 | }, 51 | data: { 52 | ended: req.body.ended, 53 | }, 54 | }); 55 | 56 | res.statusCode = 200; 57 | res.setHeader('Content-Type', 'application/json'); 58 | res.end(JSON.stringify(updatedAMA)); 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@chatsift/ama", 3 | "description": "Manage AMAs in your community", 4 | "packageManager": "yarn@3.6.4", 5 | "private": true, 6 | "version": "0.0.0", 7 | "workspaces": [ 8 | "packages/*" 9 | ], 10 | "author": { 11 | "name": "DD", 12 | "email": "didinele.dev@gmail.com" 13 | }, 14 | "repository": { 15 | "type": "git", 16 | "url": "https://github.com/chatsift/ama.git" 17 | }, 18 | "bugs": { 19 | "url": "https://github.com/chatsift/ama/issues" 20 | }, 21 | "homepage": "https://github.com/chatsift/ama", 22 | "scripts": { 23 | "lint": "turbo run lint && prettier --check .", 24 | "build": "turbo run build", 25 | "format": "prettier --write .", 26 | "prisma": "dotenv -e .env prisma", 27 | "deploy-commands": "yarn build && dotenv -e .env -v DEPLOY=true -- node --enable-source-maps ./packages/bot/dist/index.js", 28 | "start-bot": "dotenv -e .env -- node --enable-source-maps ./packages/bot/dist/index.js", 29 | "start-api": "dotenv -e .env -- node --enable-source-maps ./packages/api/dist/index.js", 30 | "prepare": "is-ci || husky install", 31 | "update": "yarn upgrade-interactive" 32 | }, 33 | "devDependencies": { 34 | "@commitlint/cli": "^17.7.2", 35 | "@commitlint/config-angular": "^17.7.0", 36 | "@typescript-eslint/eslint-plugin": "^6.7.5", 37 | "@typescript-eslint/parser": "^6.7.5", 38 | "dotenv-cli": "^7.3.0", 39 | "eslint": "^8.51.0", 40 | "eslint-config-neon": "^0.1.57", 41 | "eslint-plugin-typescript-sort-keys": "^3.0.0", 42 | "husky": "^8.0.3", 43 | "is-ci": "^3.0.1", 44 | "prettier": "^3.0.3", 45 | "prettier-eslint": "^15.0.1", 46 | "prisma": "^5.4.2", 47 | "turbo": "1.10.15", 48 | "typescript": "^5.2.2" 49 | }, 50 | "husky": { 51 | "hooks": { 52 | "pre-commit": "lint-staged" 53 | } 54 | }, 55 | "lint-staged": { 56 | "*.ts": [ 57 | "eslint --fix" 58 | ], 59 | "*.tsx": [ 60 | "eslint --fix" 61 | ] 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/api/src/index.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata'; 2 | import { dirname, join } from 'node:path'; 3 | import { fileURLToPath, pathToFileURL } from 'node:url'; 4 | import { readdirRecurse } from '@chatsift/readdir'; 5 | import type { Route } from '@chatsift/rest-utils'; 6 | import { attachHttpUtils, sendBoom } from '@chatsift/rest-utils'; 7 | import { Boom, isBoom, notFound } from '@hapi/boom'; 8 | import { PrismaClient } from '@prisma/client'; 9 | import cors from 'cors'; 10 | import helmet from 'helmet'; 11 | import type { Middleware } from 'polka'; 12 | import polka from 'polka'; 13 | import { container } from 'tsyringe'; 14 | import { Env } from './util/env.js'; 15 | import { logger } from './util/logger.js'; 16 | 17 | const env = container.resolve(Env); 18 | container.register(PrismaClient, { useValue: new PrismaClient() }); 19 | 20 | const app = polka({ 21 | onError(err, _, res) { 22 | res.setHeader('content-type', 'application/json'); 23 | const boom = isBoom(err) ? err : new Boom(err); 24 | 25 | if (boom.output.statusCode === 500) { 26 | logger.error(boom, boom.message); 27 | } 28 | 29 | sendBoom(boom, res); 30 | }, 31 | onNoMatch(_, res) { 32 | res.setHeader('content-type', 'application/json'); 33 | sendBoom(notFound(), res); 34 | }, 35 | }).use( 36 | cors({ 37 | origin: env.cors, 38 | credentials: true, 39 | }), 40 | helmet({ contentSecurityPolicy: env.isProd ? undefined : false }) as Middleware, 41 | attachHttpUtils(), 42 | ); 43 | 44 | const path = join(dirname(fileURLToPath(import.meta.url)), 'routes'); 45 | const files = readdirRecurse(path, { fileExtensions: ['js'] }); 46 | 47 | for await (const file of files) { 48 | const mod = (await import(pathToFileURL(file).toString())) as { default?: new () => Route }; 49 | if (mod.default) { 50 | const route = container.resolve(mod.default); 51 | logger.info(route.info, 'Registering route'); 52 | route.register(app); 53 | } 54 | } 55 | 56 | app.listen(env.port, () => logger.info(`Listening to requests on port ${env.port}`)); 57 | 58 | export * from './routeTypes.js'; 59 | -------------------------------------------------------------------------------- /packages/bot/src/components/mod-flag.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | import type { ButtonInteraction } from 'discord.js'; 3 | import { Client } from 'discord.js'; 4 | import { singleton } from 'tsyringe'; 5 | import { AmaManager } from '../struct/AmaManager.js'; 6 | import type { Component } from '../struct/Component'; 7 | import { Colors } from '../util/colors.js'; 8 | 9 | @singleton() 10 | export default class implements Component> { 11 | public constructor( 12 | private readonly prisma: PrismaClient, 13 | private readonly client: Client, 14 | private readonly amaManager: AmaManager, 15 | ) {} 16 | 17 | public async handle(interaction: ButtonInteraction<'cached'>, rawQuestionId: string) { 18 | const questionId = Number.parseInt(rawQuestionId, 10); 19 | const question = await this.prisma.amaQuestion.findFirst({ 20 | where: { id: questionId }, 21 | include: { ama: true }, 22 | }); 23 | 24 | if (!question) { 25 | return interaction.reply({ 26 | content: 'No AMA found, this is likely a bug.', 27 | ephemeral: true, 28 | }); 29 | } 30 | 31 | if (question.ama.ended) { 32 | return interaction.reply({ 33 | content: 'This AMA has already ended.', 34 | ephemeral: true, 35 | }); 36 | } 37 | 38 | if (!question.ama.flaggedQueue) { 39 | return interaction.reply({ 40 | content: 'This AMA has no flag queue, this is likely a bug.', 41 | ephemeral: true, 42 | }); 43 | } 44 | 45 | const member = await interaction.guild.members.fetch(interaction.user.id).catch(() => null); 46 | const user = member?.user ?? (await this.client.users.fetch(question.authorId).catch(() => null)); 47 | const result = await this.amaManager.postToFlaggedQueue({ 48 | content: question.content, 49 | imageUrl: question.imageUrl, 50 | user, 51 | question, 52 | flaggedQueue: question.ama.flaggedQueue, 53 | }); 54 | 55 | if (result.isErr()) { 56 | return interaction.reply({ 57 | content: result.unwrapErr().message, 58 | ephemeral: true, 59 | }); 60 | } 61 | 62 | return interaction.update({ 63 | embeds: [ 64 | { 65 | ...interaction.message.embeds[0]?.toJSON(), 66 | color: Colors.Flagged, 67 | }, 68 | ], 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/bot/src/components/guest-approve.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | import type { ButtonInteraction } from 'discord.js'; 3 | import { Client } from 'discord.js'; 4 | import { singleton } from 'tsyringe'; 5 | import { AmaManager } from '../struct/AmaManager.js'; 6 | import type { Component } from '../struct/Component.js'; 7 | import { Colors } from '../util/colors.js'; 8 | 9 | @singleton() 10 | export default class implements Component> { 11 | public constructor( 12 | private readonly prisma: PrismaClient, 13 | private readonly client: Client, 14 | private readonly amaManager: AmaManager, 15 | ) {} 16 | 17 | public async handle(interaction: ButtonInteraction<'cached'>, rawQuestionId: string, mode: 'stage' | 'text') { 18 | const questionId = Number.parseInt(rawQuestionId, 10); 19 | const question = await this.prisma.amaQuestion.findFirst({ 20 | where: { id: questionId }, 21 | include: { ama: true }, 22 | }); 23 | 24 | if (!question) { 25 | return interaction.reply({ 26 | content: 'No AMA found, this is likely a bug.', 27 | ephemeral: true, 28 | }); 29 | } 30 | 31 | if (question.ama.ended) { 32 | return interaction.reply({ 33 | content: 'This AMA has already ended.', 34 | ephemeral: true, 35 | }); 36 | } 37 | 38 | const member = await interaction.guild.members.fetch(question.authorId).catch(() => null); 39 | const user = member?.user ?? (await this.client.users.fetch(question.authorId).catch(() => null)); 40 | const result = await this.amaManager.postToAnswersChannel({ 41 | content: question.content, 42 | imageUrl: question.imageUrl, 43 | user, 44 | member, 45 | question, 46 | answersChannel: question.ama.answersChannel, 47 | stage: mode === 'stage', 48 | }); 49 | 50 | if (result.isErr()) { 51 | return interaction.reply({ 52 | content: result.unwrapErr().message, 53 | ephemeral: true, 54 | }); 55 | } else { 56 | await this.prisma.amaQuestion.update({ 57 | where: { 58 | id: question.id, 59 | }, 60 | data: { 61 | answerMessageId: result.unwrap().id, 62 | }, 63 | }); 64 | } 65 | 66 | return interaction.update({ 67 | embeds: [ 68 | { 69 | ...interaction.message.embeds[0]?.toJSON(), 70 | color: Colors.Approved, 71 | }, 72 | ], 73 | }); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /packages/bot/src/components/mod-approve.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | import type { ButtonInteraction } from 'discord.js'; 3 | import { Client } from 'discord.js'; 4 | import { singleton } from 'tsyringe'; 5 | import { AmaManager } from '../struct/AmaManager.js'; 6 | import type { Component } from '../struct/Component.js'; 7 | import { Colors } from '../util/colors.js'; 8 | 9 | @singleton() 10 | export default class implements Component> { 11 | public constructor( 12 | private readonly prisma: PrismaClient, 13 | private readonly client: Client, 14 | private readonly amaManager: AmaManager, 15 | ) {} 16 | 17 | public async handle(interaction: ButtonInteraction<'cached'>, rawQuestionId: string) { 18 | const questionId = Number.parseInt(rawQuestionId, 10); 19 | const question = await this.prisma.amaQuestion.findFirst({ 20 | where: { id: questionId }, 21 | include: { ama: true }, 22 | }); 23 | 24 | if (!question) { 25 | return interaction.reply({ 26 | content: 'No AMA found, this is likely a bug.', 27 | ephemeral: true, 28 | }); 29 | } 30 | 31 | if (question.ama.ended) { 32 | return interaction.reply({ 33 | content: 'This AMA has already ended.', 34 | ephemeral: true, 35 | }); 36 | } 37 | 38 | const member = await interaction.guild.members.fetch(question.authorId).catch(() => null); 39 | const user = member?.user ?? (await this.client.users.fetch(question.authorId).catch(() => null)); 40 | 41 | if (question.ama.guestQueue) { 42 | const result = await this.amaManager.postToGuestQueue({ 43 | content: question.content, 44 | imageUrl: question.imageUrl, 45 | user, 46 | member, 47 | question, 48 | guestQueue: question.ama.guestQueue, 49 | }); 50 | 51 | if (result.isErr()) { 52 | throw result.unwrapErr(); 53 | } 54 | } else { 55 | const result = await this.amaManager.postToAnswersChannel({ 56 | content: question.content, 57 | imageUrl: question.imageUrl, 58 | user, 59 | question, 60 | answersChannel: question.ama.answersChannel, 61 | stage: false, 62 | member, 63 | }); 64 | 65 | if (result.isErr()) { 66 | return interaction.reply({ 67 | content: result.unwrapErr().message, 68 | ephemeral: true, 69 | }); 70 | } else { 71 | await this.prisma.amaQuestion.update({ 72 | where: { 73 | id: question.id, 74 | }, 75 | data: { 76 | answerMessageId: result.unwrap().id, 77 | }, 78 | }); 79 | } 80 | } 81 | 82 | return interaction.update({ 83 | embeds: [ 84 | { 85 | ...interaction.message.embeds[0]?.toJSON(), 86 | color: Colors.Approved, 87 | }, 88 | ], 89 | }); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /packages/bot/src/commands/add-timestamp.ts: -------------------------------------------------------------------------------- 1 | import { ms } from '@naval-base/ms'; 2 | import { PrismaClient } from '@prisma/client'; 3 | import type { MessageContextMenuCommandInteraction, ModalActionRowComponentBuilder } from 'discord.js'; 4 | import { 5 | ActionRowBuilder, 6 | ApplicationCommandType, 7 | EmbedBuilder, 8 | ModalBuilder, 9 | TextInputBuilder, 10 | TextInputStyle, 11 | } from 'discord.js'; 12 | import { nanoid } from 'nanoid'; 13 | import { singleton } from 'tsyringe'; 14 | import type { Command, CommandBody } from '../struct/Command'; 15 | 16 | @singleton() 17 | export default class implements Command { 18 | public readonly interactionOptions: CommandBody = { 19 | name: 'Add Timestamp', 20 | type: ApplicationCommandType.Message, 21 | default_member_permissions: '0', 22 | dm_permission: false, 23 | }; 24 | 25 | public constructor(private readonly prisma: PrismaClient) {} 26 | 27 | public async handle(interaction: MessageContextMenuCommandInteraction<'cached'>) { 28 | const question = await this.prisma.amaQuestion.findFirst({ 29 | where: { 30 | answerMessageId: interaction.targetId, 31 | }, 32 | }); 33 | 34 | if (!question) { 35 | return interaction.reply({ 36 | content: 'This message is not an AMA question.', 37 | ephemeral: true, 38 | }); 39 | } 40 | 41 | const id = nanoid(); 42 | 43 | const modal = new ModalBuilder() 44 | .setTitle('Indicate when the question was answered') 45 | .setCustomId(id) 46 | .addComponents( 47 | new ActionRowBuilder().addComponents( 48 | new TextInputBuilder() 49 | .setCustomId('text') 50 | .setLabel('(Text) Visual representation of the timestamp') 51 | .setPlaceholder('e.g. 1:23:45') 52 | .setMinLength(1) 53 | .setMaxLength(20) 54 | .setStyle(TextInputStyle.Short) 55 | .setRequired(true), 56 | ), 57 | new ActionRowBuilder().addComponents( 58 | new TextInputBuilder() 59 | .setCustomId('url') 60 | .setLabel('(Optional) Link for the timestamp') 61 | .setPlaceholder('e.g. https://youtu.be/Siqi_yunMV0') 62 | .setStyle(TextInputStyle.Short) 63 | .setRequired(false), 64 | ), 65 | ); 66 | 67 | await interaction.showModal(modal); 68 | const modalInteraction = await interaction 69 | .awaitModalSubmit({ time: ms('5m'), filter: (interaction) => interaction.customId === id }) 70 | .catch(() => null); 71 | 72 | if (!modalInteraction) { 73 | return; 74 | } 75 | 76 | const text = modalInteraction.fields.getTextInputValue('text'); 77 | const url = modalInteraction.fields.getTextInputValue('url'); 78 | 79 | const answeredAt = url.length ? `[[${text}](${url})]` : `[${text}]`; 80 | 81 | const [toUpdate, ...rest] = interaction.targetMessage.embeds; 82 | const updated = new EmbedBuilder(toUpdate!.toJSON()).setDescription(`${answeredAt} ${question.content}`); 83 | 84 | await interaction.targetMessage.edit({ 85 | embeds: [updated, ...rest], 86 | }); 87 | 88 | await modalInteraction.reply({ 89 | content: 'Timestamp added!', 90 | ephemeral: true, 91 | }); 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /packages/bot/src/commands/end.ts: -------------------------------------------------------------------------------- 1 | import type { Ama } from '@prisma/client'; 2 | import { PrismaClient } from '@prisma/client'; 3 | import type { SelectMenuBuilder, SelectMenuInteraction } from 'discord.js'; 4 | import { 5 | ActionRowBuilder, 6 | SelectMenuOptionBuilder, 7 | ApplicationCommandType, 8 | type ChatInputCommandInteraction, 9 | } from 'discord.js'; 10 | import { singleton } from 'tsyringe'; 11 | import type { Command, CommandBody } from '../struct/Command'; 12 | import { SelectMenuPaginator, type SelectMenuPaginatorConsumers } from '../struct/SelectMenuPaginator.js'; 13 | 14 | @singleton() 15 | export default class implements Command { 16 | public readonly interactionOptions: CommandBody = { 17 | name: 'end', 18 | description: 'Ends an AMA', 19 | type: ApplicationCommandType.ChatInput, 20 | default_member_permissions: '0', 21 | dm_permission: false, 22 | }; 23 | 24 | public constructor(private readonly prisma: PrismaClient) {} 25 | 26 | public async handle(interaction: ChatInputCommandInteraction<'cached'>) { 27 | const amas = await this.prisma.ama.findMany({ 28 | where: { 29 | guildId: interaction.guild.id, 30 | ended: false, 31 | }, 32 | }); 33 | 34 | if (!amas.length) { 35 | return interaction.reply('No ongoing AMAs.'); 36 | } 37 | 38 | const paginator = new SelectMenuPaginator({ 39 | key: 'ama-list', 40 | data: amas, 41 | maxPageLength: 40, 42 | }); 43 | 44 | let content; 45 | const actionRow = new ActionRowBuilder(); 46 | 47 | const updateMessagePayload = (consumers: SelectMenuPaginatorConsumers) => { 48 | const { data, currentPage, selectMenu, pageLeftOption, pageRightOption } = consumers.asSelectMenu(); 49 | content = `Select an AMA to end; Page ${currentPage + 1}/${paginator.pageCount}`; 50 | 51 | const options: SelectMenuOptionBuilder[] = []; 52 | if (pageLeftOption) { 53 | options.push(pageLeftOption); 54 | } 55 | 56 | options.push(...data.map((ama) => new SelectMenuOptionBuilder().setLabel(ama.title).setValue(String(ama.id)))); 57 | 58 | if (pageRightOption) { 59 | options.push(pageRightOption); 60 | } 61 | 62 | actionRow.setComponents(selectMenu.setOptions(options).setMinValues(1).setMaxValues(1)); 63 | }; 64 | 65 | updateMessagePayload(paginator.getCurrentPage()); 66 | 67 | const reply = await interaction.reply({ 68 | content, 69 | components: [actionRow], 70 | fetchReply: true, 71 | }); 72 | 73 | for await (const [component] of reply.createMessageComponentCollector({ idle: 30_000 })) { 74 | const isLeft = component.customId === 'page-left'; 75 | const isRight = component.customId === 'page-right'; 76 | 77 | if (isLeft || isRight) { 78 | updateMessagePayload(isLeft ? paginator.previousPage() : paginator.nextPage()); 79 | await component.update({ 80 | content, 81 | components: [actionRow], 82 | }); 83 | continue; 84 | } 85 | 86 | await this.prisma.ama.update({ 87 | data: { ended: true }, 88 | where: { id: Number((component as SelectMenuInteraction).values[0]!) }, 89 | }); 90 | 91 | return interaction.editReply({ 92 | content: 'Successfully ended AMA.', 93 | components: [], 94 | }); 95 | } 96 | 97 | return reply.edit({ 98 | content: 'Timed out...', 99 | components: [], 100 | }); 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /packages/bot/src/commands/add-answer.ts: -------------------------------------------------------------------------------- 1 | import { ms } from '@naval-base/ms'; 2 | import { PrismaClient } from '@prisma/client'; 3 | import type { Embed, MessageContextMenuCommandInteraction, ModalActionRowComponentBuilder } from 'discord.js'; 4 | import { 5 | ActionRowBuilder, 6 | ApplicationCommandType, 7 | EmbedBuilder, 8 | ModalBuilder, 9 | TextInputBuilder, 10 | TextInputStyle, 11 | } from 'discord.js'; 12 | import { nanoid } from 'nanoid'; 13 | import { singleton } from 'tsyringe'; 14 | import type { Command, CommandBody } from '../struct/Command.js'; 15 | import { Colors } from '../util/colors.js'; 16 | 17 | @singleton() 18 | export default class implements Command { 19 | public readonly interactionOptions: CommandBody = { 20 | name: 'Add Answer', 21 | type: ApplicationCommandType.Message, 22 | default_member_permissions: '0', 23 | dm_permission: false, 24 | }; 25 | 26 | public constructor(private readonly prisma: PrismaClient) {} 27 | 28 | public async handle(interaction: MessageContextMenuCommandInteraction<'cached'>) { 29 | const question = await this.prisma.amaQuestion.findFirst({ 30 | where: { 31 | answerMessageId: interaction.targetId, 32 | }, 33 | }); 34 | 35 | if (!question) { 36 | return interaction.reply({ 37 | content: 'This message is not an AMA question.', 38 | ephemeral: true, 39 | }); 40 | } 41 | 42 | const id = nanoid(); 43 | 44 | const modal = new ModalBuilder() 45 | .setTitle('Add an answer') 46 | .setCustomId(id) 47 | .addComponents( 48 | new ActionRowBuilder().addComponents( 49 | new TextInputBuilder() 50 | .setCustomId('answer') 51 | .setLabel('Answer to the question') 52 | .setPlaceholder('Yes! I love ramen!') 53 | .setMinLength(2) 54 | .setMaxLength(4_000) 55 | .setStyle(TextInputStyle.Paragraph) 56 | .setRequired(true), 57 | ), 58 | new ActionRowBuilder().addComponents( 59 | new TextInputBuilder() 60 | .setCustomId('image-url') 61 | .setLabel('(Optional) Image URL to use') 62 | .setStyle(TextInputStyle.Short) 63 | .setRequired(false), 64 | ), 65 | new ActionRowBuilder().addComponents( 66 | new TextInputBuilder() 67 | .setCustomId('user-id') 68 | .setLabel('(Optional) The user ID of who is answering') 69 | .setPlaceholder('This defaults to you if not specified!') 70 | .setStyle(TextInputStyle.Short) 71 | .setRequired(false), 72 | ), 73 | ); 74 | 75 | await interaction.showModal(modal); 76 | const modalInteraction = await interaction 77 | .awaitModalSubmit({ time: ms('5m'), filter: (interaction) => interaction.customId === id }) 78 | .catch(() => null); 79 | 80 | if (!modalInteraction) { 81 | return; 82 | } 83 | 84 | const text = modalInteraction.fields.getTextInputValue('answer'); 85 | const imageUrl = modalInteraction.fields.getTextInputValue('image-url'); 86 | const userId = modalInteraction.fields.getTextInputValue('user-id'); 87 | 88 | const answeredBy = userId ? await interaction.guild.members.fetch(userId).catch(() => null) : interaction.member; 89 | 90 | if (!answeredBy) { 91 | return interaction.reply({ 92 | content: 'I could not find the given user.', 93 | ephemeral: true, 94 | }); 95 | } 96 | 97 | const embeds: (Embed | EmbedBuilder)[] = interaction.targetMessage.embeds; 98 | const answerEmbed = new EmbedBuilder() 99 | .setDescription(text) 100 | .setImage(imageUrl.length ? imageUrl : null) 101 | .setFooter({ 102 | text: `${answeredBy.nickname ?? answeredBy.user.displayName} answered`, 103 | iconURL: answeredBy.displayAvatarURL(), 104 | }) 105 | .setColor(Colors.Blurple); 106 | 107 | if (embeds.length >= 2) { 108 | embeds.splice(1, 1, answerEmbed); 109 | } else { 110 | embeds.push(answerEmbed); 111 | } 112 | 113 | await interaction.targetMessage.edit({ 114 | embeds, 115 | }); 116 | 117 | await modalInteraction.reply({ 118 | content: 'Answer added!', 119 | ephemeral: true, 120 | }); 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /packages/bot/src/components/submit-question.ts: -------------------------------------------------------------------------------- 1 | import { ms } from '@naval-base/ms'; 2 | import { PrismaClient } from '@prisma/client'; 3 | import type { Result } from '@sapphire/result'; 4 | import type { ModalActionRowComponentBuilder, ButtonInteraction } from 'discord.js'; 5 | import { ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'; 6 | import { nanoid } from 'nanoid'; 7 | import { singleton } from 'tsyringe'; 8 | import { AmaManager } from '../struct/AmaManager.js'; 9 | import type { Component } from '../struct/Component'; 10 | import { GracefulTransactionFailure } from '../struct/GracefulTransactionError.js'; 11 | 12 | @singleton() 13 | export default class implements Component> { 14 | public constructor( 15 | private readonly prisma: PrismaClient, 16 | private readonly amaManager: AmaManager, 17 | ) {} 18 | 19 | public async handle(interaction: ButtonInteraction<'cached'>) { 20 | const ama = await this.prisma.ama.findFirst({ where: { promptMessageId: interaction.message.id } }); 21 | 22 | if (!ama) { 23 | await interaction.reply({ 24 | content: 'No AMA found, this is likely a bug.', 25 | ephemeral: true, 26 | }); 27 | return; 28 | } 29 | 30 | if (ama.ended) { 31 | await interaction.reply({ 32 | content: 'This AMA has already ended.', 33 | ephemeral: true, 34 | }); 35 | return; 36 | } 37 | 38 | const id = nanoid(); 39 | 40 | const modal = new ModalBuilder() 41 | .setTitle('Ask a question for the AMA') 42 | .setCustomId(id) 43 | .addComponents( 44 | new ActionRowBuilder().addComponents( 45 | new TextInputBuilder() 46 | .setCustomId('content') 47 | .setLabel('The question you want to ask') 48 | .setMinLength(15) 49 | .setMaxLength(4_000) 50 | .setStyle(TextInputStyle.Paragraph) 51 | .setRequired(true), 52 | ), 53 | new ActionRowBuilder().addComponents( 54 | new TextInputBuilder() 55 | .setCustomId('image-url') 56 | .setLabel('Optional image URL to display') 57 | .setStyle(TextInputStyle.Short) 58 | .setRequired(false), 59 | ), 60 | ); 61 | 62 | await interaction.showModal(modal); 63 | const modalInteraction = await interaction 64 | .awaitModalSubmit({ time: ms('5m'), filter: (interaction) => interaction.customId === id }) 65 | .catch(() => null); 66 | if (!modalInteraction) { 67 | return; 68 | } 69 | 70 | const content = modalInteraction.fields.getTextInputValue('content'); 71 | const rawImageUrl = modalInteraction.fields.getTextInputValue('image-url'); 72 | const imageUrl = rawImageUrl.length ? rawImageUrl : null; 73 | 74 | await modalInteraction.reply({ 75 | content: 'Forwarding your question...', 76 | ephemeral: true, 77 | }); 78 | 79 | const question = await this.prisma 80 | .$transaction(async (prisma) => { 81 | const amaQuestion = await prisma.amaQuestion.create({ 82 | data: { 83 | amaId: ama.id, 84 | authorId: modalInteraction.user.id, 85 | content, 86 | imageUrl, 87 | }, 88 | }); 89 | 90 | const basePostData = { 91 | question: amaQuestion, 92 | content, 93 | imageUrl, 94 | user: modalInteraction.user, 95 | member: modalInteraction.member, 96 | }; 97 | 98 | // eslint-disable-next-line unicorn/consistent-function-scoping 99 | const unwrapErr = (result: Result): T => { 100 | if (result.isErr()) { 101 | const err = result.unwrapErr(); 102 | throw new GracefulTransactionFailure(err.message, { cause: err }); 103 | } 104 | 105 | return result.unwrap(); 106 | }; 107 | 108 | if (ama.modQueue) { 109 | unwrapErr( 110 | await this.amaManager.postToModQueue({ 111 | ...basePostData, 112 | modQueue: ama.modQueue, 113 | flaggedQueue: ama.flaggedQueue, 114 | }), 115 | ); 116 | } else if (ama.guestQueue) { 117 | unwrapErr( 118 | await this.amaManager.postToGuestQueue({ 119 | ...basePostData, 120 | guestQueue: ama.guestQueue, 121 | }), 122 | ); 123 | } else { 124 | const message = unwrapErr( 125 | await this.amaManager.postToAnswersChannel({ 126 | ...basePostData, 127 | answersChannel: ama.answersChannel, 128 | stage: ama.stageOnly, 129 | }), 130 | ); 131 | 132 | if (message) { 133 | await prisma.amaQuestion.update({ 134 | where: { 135 | id: amaQuestion.id, 136 | }, 137 | data: { 138 | answerMessageId: message.id, 139 | }, 140 | }); 141 | } 142 | } 143 | 144 | return amaQuestion; 145 | }) 146 | .catch(async (error) => { 147 | if (error instanceof GracefulTransactionFailure) { 148 | await modalInteraction.editReply({ content: error.message }); 149 | return null; 150 | } 151 | 152 | throw error; 153 | }); 154 | 155 | if (!question) { 156 | return; 157 | } 158 | 159 | await modalInteraction.editReply({ content: 'Question sent!' }); 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /packages/bot/src/struct/AmaManager.ts: -------------------------------------------------------------------------------- 1 | import type { AmaQuestion } from '@prisma/client'; 2 | import { Result } from '@sapphire/result'; 3 | import type { GuildMember, Message, MessageActionRowComponentBuilder, TextChannel, User } from 'discord.js'; 4 | import { ActionRowBuilder, ButtonBuilder, EmbedBuilder, ButtonStyle, Client } from 'discord.js'; 5 | import { singleton } from 'tsyringe'; 6 | import { Colors } from '../util/colors.js'; 7 | 8 | export interface EmbedData { 9 | content: string; 10 | displayId?: boolean; 11 | imageUrl?: string | null; 12 | member?: GuildMember | null; 13 | user?: User | null; 14 | } 15 | 16 | export interface PostData extends EmbedData { 17 | question: AmaQuestion; 18 | } 19 | 20 | export interface PostToModQueueData extends PostData { 21 | flaggedQueue: string | null; 22 | modQueue: string; 23 | } 24 | 25 | export interface PostToFlaggedQueueData extends PostData { 26 | flaggedQueue: string; 27 | } 28 | 29 | export interface PostToGuestQueueData extends PostData { 30 | guestQueue: string; 31 | } 32 | 33 | export interface PostToAnswerChannelData extends PostData { 34 | answersChannel: string; 35 | /** 36 | * @deprecated We no longer distinguish between stage and non-stage answers/AMAs 37 | */ 38 | stage: boolean; 39 | } 40 | 41 | @singleton() 42 | export class AmaManager { 43 | public constructor(private readonly client: Client) {} 44 | 45 | private getBaseEmbed({ content, imageUrl, user, member, displayId = true }: EmbedData): EmbedBuilder { 46 | const embed = new EmbedBuilder() 47 | .setDescription(content) 48 | .setImage(imageUrl ?? null) 49 | .setAuthor({ 50 | name: member?.nickname ?? user?.tag ?? 'Unknown User', 51 | iconURL: member?.displayAvatarURL() ?? user?.displayAvatarURL(), 52 | }); 53 | 54 | if (displayId) { 55 | embed.setFooter({ 56 | text: `${user?.tag ?? 'Unknown User'} (${user?.id ?? 'Unknown - likely deleted user'})`, 57 | iconURL: user?.displayAvatarURL(), 58 | }); 59 | } 60 | 61 | return embed; 62 | } 63 | 64 | public async postToModQueue({ 65 | question, 66 | modQueue, 67 | flaggedQueue, 68 | ...embedData 69 | }: PostToModQueueData): Promise> { 70 | const row = new ActionRowBuilder().addComponents( 71 | new ButtonBuilder().setLabel('Approve').setStyle(ButtonStyle.Success).setCustomId(`mod-approve|${question.id}`), 72 | new ButtonBuilder().setLabel('Deny').setStyle(ButtonStyle.Danger).setCustomId(`mod-deny|${question.id}`), 73 | ); 74 | 75 | const channel = (await this.client.channels.fetch(modQueue).catch(() => null)) as TextChannel | null; 76 | if (!channel) { 77 | return Result.err(new Error('The mod queue channel no longer exists - please contact an admin.')); 78 | } 79 | 80 | if (flaggedQueue) { 81 | row.addComponents( 82 | new ButtonBuilder() 83 | .setLabel('Flag') 84 | .setStyle(ButtonStyle.Secondary) 85 | .setCustomId(`mod-flag|${question.id}`) 86 | .setEmoji({ name: '⚠️' }), 87 | ); 88 | } 89 | 90 | await channel.send({ 91 | allowedMentions: { parse: [] }, 92 | embeds: [this.getBaseEmbed(embedData)], 93 | components: [row], 94 | }); 95 | 96 | return Result.ok(); 97 | } 98 | 99 | public async postToFlaggedQueue({ 100 | question, 101 | flaggedQueue, 102 | ...embedData 103 | }: PostToFlaggedQueueData): Promise> { 104 | const channel = (await this.client.channels.fetch(flaggedQueue).catch(() => null)) as TextChannel | null; 105 | if (!channel) { 106 | return Result.err(new Error('The flagged queue channel no longer exists - please contact an admin.')); 107 | } 108 | 109 | await channel.send({ 110 | allowedMentions: { parse: [] }, 111 | embeds: [this.getBaseEmbed(embedData)], 112 | }); 113 | 114 | return Result.ok(); 115 | } 116 | 117 | public async postToGuestQueue({ 118 | question, 119 | guestQueue, 120 | ...embedData 121 | }: PostToGuestQueueData): Promise> { 122 | const row = new ActionRowBuilder().addComponents( 123 | new ButtonBuilder().setLabel('Answer').setStyle(ButtonStyle.Success).setCustomId(`guest-approve|${question.id}`), 124 | new ButtonBuilder().setLabel('Skip').setStyle(ButtonStyle.Danger).setCustomId(`guest-deny|${question.id}`), 125 | ); 126 | 127 | const channel = (await this.client.channels.fetch(guestQueue).catch(() => null)) as TextChannel | null; 128 | if (!channel) { 129 | return Result.err(new Error('The guest queue channel no longer exists - please contact an admin.')); 130 | } 131 | 132 | await channel.send({ 133 | allowedMentions: { parse: [] }, 134 | embeds: [this.getBaseEmbed({ ...embedData, displayId: false })], 135 | components: [row], 136 | }); 137 | 138 | return Result.ok(); 139 | } 140 | 141 | public async postToAnswersChannel({ 142 | question, 143 | stage, 144 | answersChannel, 145 | ...embedData 146 | }: PostToAnswerChannelData): Promise, Error>> { 147 | const embed = this.getBaseEmbed({ ...embedData, displayId: false }); 148 | embed.setColor(Colors.Blurple); 149 | 150 | // This is deprecated 151 | if (stage) { 152 | embed.setFooter({ text: 'This question was answered via stage' }); 153 | } 154 | 155 | const channel = (await this.client.channels.fetch(answersChannel).catch(() => null)) as TextChannel | null; 156 | if (!channel) { 157 | return Result.err(new Error('The answers channel no longer exists - please contact an admin.')); 158 | } 159 | 160 | const message = await channel.send({ 161 | allowedMentions: { parse: [] }, 162 | embeds: [embed], 163 | }); 164 | 165 | return Result.ok(message); 166 | } 167 | } 168 | -------------------------------------------------------------------------------- /packages/bot/src/struct/CommandHandler.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable consistent-return */ 2 | import { dirname, join } from 'node:path'; 3 | import { fileURLToPath, pathToFileURL } from 'node:url'; 4 | import { readdirRecurse } from '@chatsift/readdir'; 5 | import { REST } from '@discordjs/rest'; 6 | import type { AutocompleteInteraction, CommandInteraction, MessageComponentInteraction } from 'discord.js'; 7 | import { inlineCode, Routes } from 'discord.js'; 8 | import { container, singleton } from 'tsyringe'; 9 | import { logger } from '../util/logger.js'; 10 | import type { Command, CommandConstructor } from './Command.js'; 11 | import { getComponentInfo, type ComponentConstructor, type Component } from './Component.js'; 12 | import { Env } from './Env.js'; 13 | 14 | @singleton() 15 | export class CommandHandler { 16 | public readonly commands = new Map(); 17 | 18 | public readonly components = new Map(); 19 | 20 | public constructor(private readonly env: Env) {} 21 | 22 | public async handleAutocomplete(interaction: AutocompleteInteraction) { 23 | const command = this.commands.get(interaction.commandName); 24 | 25 | if (!command?.handleAutocomplete) { 26 | return interaction.respond([]); 27 | } 28 | 29 | if (command.interactionOptions.dm_permission && interaction.inCachedGuild()) { 30 | return; 31 | } 32 | 33 | try { 34 | const options = await command.handleAutocomplete(interaction); 35 | await interaction.respond(options.slice(0, 25)); 36 | } catch (error) { 37 | logger.error( 38 | { 39 | err: error, 40 | command: interaction.commandName, 41 | }, 42 | 'Error handling autocomplete', 43 | ); 44 | return interaction.respond([ 45 | { 46 | name: 'Something went wrong fetching auto complete options. Please report this bug.', 47 | value: 'noop', 48 | }, 49 | ]); 50 | } 51 | } 52 | 53 | public async handleMessageComponent(interaction: MessageComponentInteraction<'cached'>) { 54 | const [name, ...args] = interaction.customId.split('|') as [string, ...string[]]; 55 | const component = this.components.get(name); 56 | 57 | try { 58 | // eslint-disable-next-line @typescript-eslint/return-await 59 | return await component?.handle(interaction, ...args); 60 | } catch (error) { 61 | logger.error( 62 | { 63 | err: error, 64 | component: name, 65 | }, 66 | 'Error handling message component', 67 | ); 68 | const content = `Something went wrong running component. Please report this bug.\n\n${inlineCode( 69 | error as Error['message'], 70 | )}`; 71 | 72 | await interaction.reply({ content, ephemeral: true }).catch(() => null); 73 | await interaction.followUp({ content, ephemeral: true }).catch(() => null); 74 | } 75 | } 76 | 77 | public async handleCommand(interaction: CommandInteraction) { 78 | const command = this.commands.get(interaction.commandName); 79 | if (!command) { 80 | logger.warn(interaction, 'Command interaction not registered locally was not chatInput'); 81 | return interaction.reply('Command not found. This is most certainly a bug'); 82 | } 83 | 84 | if (!command.interactionOptions.dm_permission && !interaction.inCachedGuild()) { 85 | logger.warn( 86 | { 87 | interaction, 88 | command, 89 | }, 90 | 'Command interaction had dm_permission off and was not in cached guild', 91 | ); 92 | return; 93 | } 94 | 95 | try { 96 | // @ts-expect-error - Yet another instance of odd union behavior. Unsure if there's a way to avoid this 97 | // eslint-disable-next-line @typescript-eslint/return-await 98 | return await command.handle(interaction); 99 | } catch (error) { 100 | // TODO(DD): Consider dealing with specific error 101 | logger.error( 102 | { 103 | err: error, 104 | command: interaction.commandName, 105 | }, 106 | 'Error handling command', 107 | ); 108 | const content = `Something went wrong running command. This could be a bug, or it could be related to your permissions.\n\n${inlineCode( 109 | error as Error['message'], 110 | )}`; 111 | 112 | // Try to display something to the user. 113 | if (interaction.replied) { 114 | await interaction.followUp({ content, ephemeral: true }); 115 | return; 116 | } 117 | 118 | await interaction.reply({ content, ephemeral: true }).catch(() => null); 119 | await interaction.editReply({ content }).catch(() => null); 120 | } 121 | } 122 | 123 | // eslint-disable-next-line @typescript-eslint/no-invalid-void-type 124 | public async init(): Promise { 125 | return Promise.all([this.registerCommands(), this.registerComponents()]); 126 | } 127 | 128 | public async registerInteractions(): Promise { 129 | const api = new REST().setToken(this.env.discordToken); 130 | const options = [...this.commands.values()].map((command) => command.interactionOptions); 131 | await api.put(Routes.applicationCommands(this.env.discordClientId), { body: options }); 132 | } 133 | 134 | private async registerCommands(): Promise { 135 | const path = join(dirname(fileURLToPath(import.meta.url)), '..', 'commands'); 136 | const files = readdirRecurse(path, { fileExtensions: ['js'] }); 137 | 138 | for await (const file of files) { 139 | const mod = (await import(pathToFileURL(file).toString())) as { default: CommandConstructor }; 140 | const command = container.resolve(mod.default); 141 | 142 | this.commands.set(command.interactionOptions.name, command); 143 | } 144 | } 145 | 146 | private async registerComponents(): Promise { 147 | const path = join(dirname(fileURLToPath(import.meta.url)), '..', 'components'); 148 | const files = readdirRecurse(path, { fileExtensions: ['js'] }); 149 | 150 | for await (const file of files) { 151 | const info = getComponentInfo(file); 152 | if (!info) { 153 | continue; 154 | } 155 | 156 | const mod = (await import(pathToFileURL(file).toString())) as { default: ComponentConstructor }; 157 | const component = container.resolve(mod.default); 158 | const name = component.name ?? info.name; 159 | 160 | this.components.set(name, component); 161 | } 162 | } 163 | } 164 | -------------------------------------------------------------------------------- /packages/bot/src/commands/start.ts: -------------------------------------------------------------------------------- 1 | import { ms } from '@naval-base/ms'; 2 | import { PrismaClient } from '@prisma/client'; 3 | import type { MessageActionRowComponentBuilder, ModalActionRowComponentBuilder } from 'discord.js'; 4 | import { 5 | ActionRowBuilder, 6 | ButtonBuilder, 7 | EmbedBuilder, 8 | ModalBuilder, 9 | TextInputBuilder, 10 | ApplicationCommandOptionType, 11 | ApplicationCommandType, 12 | ButtonStyle, 13 | ChannelType, 14 | TextInputStyle, 15 | type ChatInputCommandInteraction, 16 | } from 'discord.js'; 17 | import { nanoid } from 'nanoid'; 18 | import { singleton } from 'tsyringe'; 19 | import type { Command, CommandBody } from '../struct/Command'; 20 | import { Colors } from '../util/colors.js'; 21 | 22 | const allowedChannelTypes: Exclude[] = [ 23 | ChannelType.GuildText, 24 | ChannelType.AnnouncementThread, 25 | ChannelType.PublicThread, 26 | ChannelType.PrivateThread, 27 | ]; 28 | 29 | @singleton() 30 | export default class implements Command { 31 | public readonly interactionOptions: CommandBody = { 32 | name: 'start', 33 | description: 'Starts an AMA session', 34 | type: ApplicationCommandType.ChatInput, 35 | default_member_permissions: '0', 36 | dm_permission: false, 37 | options: [ 38 | { 39 | name: 'answers-channel', 40 | description: 'Channel to use for answers', 41 | type: ApplicationCommandOptionType.Channel, 42 | channel_types: allowedChannelTypes, 43 | required: true, 44 | }, 45 | { 46 | name: 'mod-queue', 47 | description: 'Channel to use for the mod queue', 48 | type: ApplicationCommandOptionType.Channel, 49 | channel_types: allowedChannelTypes, 50 | }, 51 | { 52 | name: 'flagged-queue', 53 | description: 'Channel to use for flagged messages', 54 | type: ApplicationCommandOptionType.Channel, 55 | channel_types: allowedChannelTypes, 56 | }, 57 | { 58 | name: 'guest-queue', 59 | description: 'Channel to use for the guest queue', 60 | type: ApplicationCommandOptionType.Channel, 61 | channel_types: allowedChannelTypes, 62 | }, 63 | ], 64 | }; 65 | 66 | public constructor(private readonly prisma: PrismaClient) {} 67 | 68 | public async handle(interaction: ChatInputCommandInteraction<'cached'>) { 69 | const modQueue = interaction.options.getChannel('mod-queue')?.id; 70 | const flaggedQueue = interaction.options.getChannel('flagged-queue')?.id; 71 | const guestQueue = interaction.options.getChannel('guest-queue')?.id; 72 | const answersChannel = interaction.options.getChannel('answers-channel', true).id; 73 | 74 | if (!modQueue && flaggedQueue) { 75 | await interaction.reply('You cannot specify a flagged queue without a mod queue'); 76 | return; 77 | } 78 | 79 | const id = nanoid(); 80 | 81 | const modal = new ModalBuilder() 82 | .setTitle('Start an AMA session') 83 | .setCustomId(id) 84 | .addComponents( 85 | new ActionRowBuilder().addComponents( 86 | new TextInputBuilder() 87 | .setCustomId('title') 88 | .setLabel('Title of your AMA') 89 | .setPlaceholder('AMA with renowed JP VA John Doe') 90 | .setMinLength(1) 91 | .setMaxLength(1_000) 92 | .setStyle(TextInputStyle.Short) 93 | .setRequired(true), 94 | ), 95 | new ActionRowBuilder().addComponents( 96 | new TextInputBuilder() 97 | .setCustomId('description') 98 | .setLabel('Optional brief description of your AMA/guest') 99 | .setPlaceholder('John Doe debut in 2010, he is currently voicing in the hit series "Morbius: The Return"') 100 | .setMaxLength(4_000) 101 | .setStyle(TextInputStyle.Paragraph) 102 | .setRequired(false), 103 | ), 104 | new ActionRowBuilder().addComponents( 105 | new TextInputBuilder() 106 | .setCustomId('plain-text') 107 | .setLabel('Optional text outside of the embed for pings') 108 | .setPlaceholder('You might need to do something like <@&123456789> to actually ping') 109 | .setMaxLength(100) 110 | .setStyle(TextInputStyle.Paragraph) 111 | .setRequired(false), 112 | ), 113 | new ActionRowBuilder().addComponents( 114 | new TextInputBuilder() 115 | .setCustomId('image-url') 116 | .setLabel('Optional image URL to use') 117 | .setStyle(TextInputStyle.Short) 118 | .setRequired(false), 119 | ), 120 | new ActionRowBuilder().addComponents( 121 | new TextInputBuilder() 122 | .setCustomId('thumbnail-url') 123 | .setLabel('Optional thumbnail URL to use') 124 | .setStyle(TextInputStyle.Short) 125 | .setRequired(false), 126 | ), 127 | ); 128 | 129 | await interaction.showModal(modal); 130 | const modalInteraction = await interaction 131 | .awaitModalSubmit({ time: ms('5m'), filter: (interaction) => interaction.customId === id }) 132 | .catch(() => null); 133 | if (!modalInteraction) { 134 | return; 135 | } 136 | 137 | await modalInteraction.reply({ 138 | content: 'Creating AMA session...', 139 | ephemeral: true, 140 | }); 141 | 142 | const title = modalInteraction.fields.getTextInputValue('title'); 143 | const plainText = modalInteraction.fields.getTextInputValue('plain-text'); 144 | const description = modalInteraction.fields.getTextInputValue('description'); 145 | const imageUrl = modalInteraction.fields.getTextInputValue('image-url'); 146 | const thumbnailUrl = modalInteraction.fields.getTextInputValue('thumbnail-url'); 147 | 148 | const promptMessage = await interaction.channel!.send({ 149 | content: plainText.length ? plainText : undefined, 150 | embeds: [ 151 | new EmbedBuilder() 152 | .setColor(Colors.Blurple) 153 | .setTitle(title) 154 | .setDescription(description.length ? description : null) 155 | .setImage(imageUrl.length ? imageUrl : null) 156 | .setThumbnail(thumbnailUrl.length ? thumbnailUrl : null), 157 | ], 158 | components: [ 159 | new ActionRowBuilder().addComponents( 160 | new ButtonBuilder() 161 | .setCustomId('submit-question') 162 | .setLabel('Submit a question') 163 | .setStyle(ButtonStyle.Primary), 164 | ), 165 | ], 166 | }); 167 | 168 | await this.prisma.ama.create({ 169 | data: { 170 | guildId: interaction.guildId, 171 | modQueue, 172 | flaggedQueue, 173 | guestQueue, 174 | title, 175 | answersChannel, 176 | promptChannelId: interaction.channel!.id, 177 | promptMessageId: promptMessage.id, 178 | }, 179 | }); 180 | } 181 | } 182 | -------------------------------------------------------------------------------- /packages/bot/src/struct/SelectMenuPaginator.ts: -------------------------------------------------------------------------------- 1 | import type { If } from 'discord.js'; 2 | import { ButtonBuilder, SelectMenuBuilder, ButtonStyle, SelectMenuOptionBuilder } from 'discord.js'; 3 | 4 | export interface SelectMenuPaginatorState { 5 | currentPage: number; 6 | readonly data: T; 7 | } 8 | 9 | export interface SelectMenuPaginatorOptions { 10 | data?: T; 11 | key: string; 12 | maxPageLength?: number; 13 | store?: Map>; 14 | } 15 | 16 | type BaseSelectMenuPaginatorData = SelectMenuPaginatorState & { 17 | selectMenu: SelectMenuBuilder; 18 | }; 19 | 20 | type SelectMenuOptionsSelectMenuPaginatorData = BaseSelectMenuPaginatorData & { 21 | pageLeftOption?: SelectMenuOptionBuilder; 22 | pageRightOption?: SelectMenuOptionBuilder; 23 | }; 24 | 25 | type ButtonsSelectMenuPaginatorData = BaseSelectMenuPaginatorData & { 26 | pageLeftButton: ButtonBuilder; 27 | pageRightButton: ButtonBuilder; 28 | }; 29 | 30 | export interface SelectMenuPaginatorConsumers { 31 | asButtons(): ButtonsSelectMenuPaginatorData; 32 | asSelectMenu(): SelectMenuOptionsSelectMenuPaginatorData; 33 | } 34 | 35 | export class SelectMenuPaginator { 36 | private readonly key: string; 37 | 38 | private state!: If>; 39 | 40 | private readonly store?: Map>; 41 | 42 | private readonly maxPageLength: number; 43 | 44 | public constructor(options: SelectMenuPaginatorOptions & { data?: Data }) { 45 | if (options.data) { 46 | // eslint-disable-next-line no-extra-parens 47 | (this as SelectMenuPaginator).state = { 48 | currentPage: 0, 49 | data: options.data, 50 | }; 51 | } else { 52 | // eslint-disable-next-line no-extra-parens 53 | (this as SelectMenuPaginator).state = null; 54 | } 55 | 56 | this.key = options.key; 57 | this.store = options.store; 58 | this.maxPageLength = options.maxPageLength ?? 25; 59 | } 60 | 61 | private isAsserted(): this is SelectMenuPaginator { 62 | return this.state !== null; 63 | } 64 | 65 | private makeConsumers(): SelectMenuPaginatorConsumers { 66 | if (!this.isAsserted()) { 67 | throw new Error('State not asserted'); 68 | } 69 | 70 | return { 71 | asButtons: () => { 72 | if (!this.isAsserted()) { 73 | throw new Error('State not asserted'); 74 | } 75 | 76 | const { currentPage, data } = this.state; 77 | const slice = data.slice( 78 | currentPage * this.maxPageLength, 79 | currentPage * this.maxPageLength + this.maxPageLength, 80 | ) as Data; 81 | 82 | return { 83 | currentPage, 84 | data: slice, 85 | selectMenu: new SelectMenuBuilder().setCustomId('select-menu').setMaxValues(slice.length), 86 | pageLeftButton: new ButtonBuilder() 87 | .setCustomId('page-left') 88 | .setStyle(ButtonStyle.Secondary) 89 | .setEmoji({ name: '◀️' }) 90 | .setDisabled(currentPage === 0), 91 | pageRightButton: new ButtonBuilder() 92 | .setCustomId('page-right') 93 | .setStyle(ButtonStyle.Secondary) 94 | .setEmoji({ name: '▶️' }) 95 | .setDisabled(currentPage === this.pageCount - 1), 96 | }; 97 | }, 98 | asSelectMenu: () => { 99 | if (!this.isAsserted()) { 100 | throw new Error('State not asserted'); 101 | } 102 | 103 | const { currentPage, data } = this.state; 104 | let offset = 0; 105 | if (currentPage === 0) { 106 | offset++; 107 | } 108 | 109 | if (currentPage === this.pageCount - 1) { 110 | offset++; 111 | } 112 | 113 | const maxPageLength = this.maxPageLength - offset; 114 | const slice = data.slice(currentPage * maxPageLength, currentPage * maxPageLength + maxPageLength) as Data; 115 | 116 | return { 117 | currentPage, 118 | data: slice, 119 | pageLeftOption: 120 | currentPage === 0 && this.pageCount > 1 121 | ? new SelectMenuOptionBuilder().setEmoji({ name: '◀️' }).setLabel('Page left').setValue('page-left') 122 | : undefined, 123 | pageRightOption: 124 | currentPage === this.pageCount - 1 && this.pageCount > 1 125 | ? new SelectMenuOptionBuilder().setEmoji({ name: '▶️' }).setLabel('Page right').setValue('page-right') 126 | : undefined, 127 | selectMenu: new SelectMenuBuilder().setCustomId('select-menu').setMinValues(1).setMaxValues(slice.length), 128 | }; 129 | }, 130 | }; 131 | } 132 | 133 | public get pageCount(): number { 134 | if (!this.isAsserted()) { 135 | throw new Error('State not asserted'); 136 | } 137 | 138 | return Math.ceil(this.state.data.length / this.maxPageLength); 139 | } 140 | 141 | // eslint-disable-next-line @typescript-eslint/prefer-return-this-type 142 | public assertState(): SelectMenuPaginator { 143 | if (this.isAsserted()) { 144 | this.store?.set(this.key, this.state); 145 | return this; 146 | } 147 | 148 | if (!this.store) { 149 | throw new Error('Either store or data are required'); 150 | } 151 | 152 | const state = this.store.get(this.key); 153 | if (!state) { 154 | throw new Error('Could not find state'); 155 | } 156 | 157 | const newThis = this as SelectMenuPaginator; 158 | newThis.state = state; 159 | return newThis; 160 | } 161 | 162 | public nextPage(): SelectMenuPaginatorConsumers { 163 | if (!this.isAsserted()) { 164 | throw new Error('State not asserted'); 165 | } 166 | 167 | if (this.state.currentPage + 1 === this.pageCount) { 168 | throw new Error('No next page'); 169 | } 170 | 171 | this.state.currentPage++; 172 | this.store?.set(this.key, this.state); 173 | 174 | return this.makeConsumers(); 175 | } 176 | 177 | public previousPage(): SelectMenuPaginatorConsumers { 178 | if (!this.isAsserted()) { 179 | throw new Error('State not asserted'); 180 | } 181 | 182 | if (this.state.currentPage === 0) { 183 | throw new Error('No previous page'); 184 | } 185 | 186 | this.state.currentPage--; 187 | this.store?.set(this.key, this.state); 188 | 189 | return this.makeConsumers(); 190 | } 191 | 192 | public setPage(page: number): SelectMenuPaginatorConsumers { 193 | if (!this.isAsserted()) { 194 | throw new Error('State not asserted'); 195 | } 196 | 197 | if (page < 0 || page >= this.pageCount) { 198 | throw new Error('Page is out of bounds'); 199 | } 200 | 201 | this.state.currentPage = page; 202 | this.store?.set(this.key, this.state); 203 | 204 | return this.makeConsumers(); 205 | } 206 | 207 | public getCurrentPage(): SelectMenuPaginatorConsumers { 208 | if (!this.isAsserted()) { 209 | throw new Error('State not asserted'); 210 | } 211 | 212 | return this.makeConsumers(); 213 | } 214 | 215 | public destroy(): void { 216 | const newThis = this as SelectMenuPaginator; 217 | newThis.state = null; 218 | this.store?.delete(this.key); 219 | } 220 | } 221 | -------------------------------------------------------------------------------- /packages/bot/src/commands/ask.ts: -------------------------------------------------------------------------------- 1 | import { ms } from '@naval-base/ms'; 2 | import type { Ama } from '@prisma/client'; 3 | import { PrismaClient } from '@prisma/client'; 4 | import type { Result } from '@sapphire/result'; 5 | import { 6 | ActionRowBuilder, 7 | ModalBuilder, 8 | SelectMenuOptionBuilder, 9 | TextInputBuilder, 10 | ApplicationCommandType, 11 | TextInputStyle, 12 | type ChatInputCommandInteraction, 13 | } from 'discord.js'; 14 | import type { ModalActionRowComponentBuilder, SelectMenuBuilder, SelectMenuInteraction } from 'discord.js'; 15 | import { singleton } from 'tsyringe'; 16 | import { AmaManager } from '../struct/AmaManager.js'; 17 | import type { Command, CommandBody } from '../struct/Command.js'; 18 | import { GracefulTransactionFailure } from '../struct/GracefulTransactionError.js'; 19 | import { SelectMenuPaginator, type SelectMenuPaginatorConsumers } from '../struct/SelectMenuPaginator.js'; 20 | 21 | @singleton() 22 | export default class implements Command { 23 | public readonly interactionOptions: CommandBody = { 24 | name: 'ask', 25 | description: 'Asks a question', 26 | type: ApplicationCommandType.ChatInput, 27 | dm_permission: false, 28 | }; 29 | 30 | public constructor( 31 | private readonly prisma: PrismaClient, 32 | private readonly amaManager: AmaManager, 33 | ) {} 34 | 35 | private async prompt(interaction: ChatInputCommandInteraction<'cached'> | SelectMenuInteraction<'cached'>, ama: Ama) { 36 | const modal = new ModalBuilder() 37 | .setTitle('Ask a question for the AMA') 38 | .setCustomId('modal') 39 | .addComponents( 40 | new ActionRowBuilder().addComponents( 41 | new TextInputBuilder() 42 | .setCustomId('content') 43 | .setLabel('The question you want to ask') 44 | .setMinLength(15) 45 | .setMaxLength(4_000) 46 | .setStyle(TextInputStyle.Paragraph) 47 | .setRequired(true), 48 | ), 49 | new ActionRowBuilder().addComponents( 50 | new TextInputBuilder() 51 | .setCustomId('image-url') 52 | .setLabel('Optional image URL to display') 53 | .setStyle(TextInputStyle.Short) 54 | .setRequired(false), 55 | ), 56 | ); 57 | 58 | await interaction.showModal(modal); 59 | const modalInteraction = await interaction.awaitModalSubmit({ time: ms('5m') }).catch(() => null); 60 | if (!modalInteraction) { 61 | return; 62 | } 63 | 64 | const content = modalInteraction.fields.getTextInputValue('content'); 65 | const rawImageUrl = modalInteraction.fields.getTextInputValue('image-url'); 66 | const imageUrl = rawImageUrl.length ? rawImageUrl : null; 67 | 68 | await modalInteraction.reply({ 69 | content: 'Forwarding your question...', 70 | ephemeral: true, 71 | }); 72 | 73 | const question = await this.prisma 74 | .$transaction(async (prisma) => { 75 | const amaQuestion = await prisma.amaQuestion.create({ 76 | data: { 77 | amaId: ama.id, 78 | authorId: modalInteraction.user.id, 79 | content, 80 | imageUrl, 81 | }, 82 | }); 83 | 84 | const basePostData = { 85 | question: amaQuestion, 86 | content, 87 | imageUrl, 88 | user: modalInteraction.user, 89 | member: modalInteraction.member, 90 | }; 91 | 92 | // eslint-disable-next-line unicorn/consistent-function-scoping 93 | const unwrapErr = (result: Result): T => { 94 | if (result.isErr()) { 95 | const err = result.unwrapErr(); 96 | throw new GracefulTransactionFailure(err.message, { cause: err }); 97 | } 98 | 99 | return result.unwrap(); 100 | }; 101 | 102 | if (ama.modQueue) { 103 | unwrapErr( 104 | await this.amaManager.postToModQueue({ 105 | ...basePostData, 106 | modQueue: ama.modQueue, 107 | flaggedQueue: ama.flaggedQueue, 108 | }), 109 | ); 110 | } else if (ama.guestQueue) { 111 | unwrapErr( 112 | await this.amaManager.postToGuestQueue({ 113 | ...basePostData, 114 | guestQueue: ama.guestQueue, 115 | }), 116 | ); 117 | } else { 118 | const message = unwrapErr( 119 | await this.amaManager.postToAnswersChannel({ 120 | ...basePostData, 121 | answersChannel: ama.answersChannel, 122 | stage: ama.stageOnly, 123 | }), 124 | ); 125 | 126 | if (message) { 127 | await prisma.amaQuestion.update({ 128 | where: { 129 | id: amaQuestion.id, 130 | }, 131 | data: { 132 | answerMessageId: message.id, 133 | }, 134 | }); 135 | } 136 | } 137 | 138 | return amaQuestion; 139 | }) 140 | .catch(async (error) => { 141 | if (error instanceof GracefulTransactionFailure) { 142 | await modalInteraction.editReply({ content: error.message }); 143 | return null; 144 | } 145 | 146 | throw error; 147 | }); 148 | 149 | if (!question) { 150 | return; 151 | } 152 | 153 | await modalInteraction.editReply({ content: 'Question sent!' }); 154 | } 155 | 156 | public async handle(interaction: ChatInputCommandInteraction<'cached'>) { 157 | const amas = await this.prisma.ama.findMany({ 158 | where: { 159 | guildId: interaction.guild.id, 160 | ended: false, 161 | }, 162 | }); 163 | 164 | if (!amas.length) { 165 | return interaction.reply({ 166 | content: 'No ongoing AMAs.', 167 | ephemeral: true, 168 | }); 169 | } 170 | 171 | if (amas.length > 1) { 172 | const paginator = new SelectMenuPaginator({ 173 | key: 'ama-list', 174 | data: amas, 175 | maxPageLength: 40, 176 | }); 177 | 178 | let content; 179 | const actionRow = new ActionRowBuilder(); 180 | 181 | const updateMessagePayload = (consumers: SelectMenuPaginatorConsumers) => { 182 | const { data, currentPage, selectMenu, pageLeftOption, pageRightOption } = consumers.asSelectMenu(); 183 | content = `Select an AMA; Page ${currentPage + 1}/${paginator.pageCount}`; 184 | 185 | const options: SelectMenuOptionBuilder[] = []; 186 | if (pageLeftOption) { 187 | options.push(pageLeftOption); 188 | } 189 | 190 | options.push(...data.map((ama) => new SelectMenuOptionBuilder().setLabel(ama.title).setValue(String(ama.id)))); 191 | 192 | if (pageRightOption) { 193 | options.push(pageRightOption); 194 | } 195 | 196 | actionRow.setComponents(selectMenu.setOptions(options).setMinValues(1).setMaxValues(1)); 197 | }; 198 | 199 | updateMessagePayload(paginator.getCurrentPage()); 200 | 201 | const reply = await interaction.reply({ 202 | content, 203 | components: [actionRow], 204 | fetchReply: true, 205 | ephemeral: true, 206 | }); 207 | 208 | for await (const [component] of reply.createMessageComponentCollector({ idle: 30_000 })) { 209 | const isLeft = component.customId === 'page-left'; 210 | const isRight = component.customId === 'page-right'; 211 | 212 | if (isLeft || isRight) { 213 | updateMessagePayload(isLeft ? paginator.previousPage() : paginator.nextPage()); 214 | await component.update({ 215 | content, 216 | components: [actionRow], 217 | }); 218 | continue; 219 | } 220 | 221 | // eslint-disable-next-line no-extra-parens 222 | const ama = amas.find((a) => a.id === Number.parseInt((component as SelectMenuInteraction).values[0]!, 10))!; 223 | return this.prompt(component as SelectMenuInteraction<'cached'>, ama); 224 | } 225 | 226 | return reply.edit({ 227 | content: 'Timed out...', 228 | components: [], 229 | }); 230 | } 231 | 232 | return this.prompt(interaction, amas[0]!); 233 | } 234 | } 235 | -------------------------------------------------------------------------------- /.yarn/plugins/@yarnpkg/plugin-workspace-tools.cjs: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | //prettier-ignore 3 | module.exports = { 4 | name: "@yarnpkg/plugin-workspace-tools", 5 | factory: function (require) { 6 | var plugin=(()=>{var wr=Object.create,me=Object.defineProperty,Sr=Object.defineProperties,vr=Object.getOwnPropertyDescriptor,Hr=Object.getOwnPropertyDescriptors,$r=Object.getOwnPropertyNames,et=Object.getOwnPropertySymbols,kr=Object.getPrototypeOf,tt=Object.prototype.hasOwnProperty,Tr=Object.prototype.propertyIsEnumerable;var rt=(e,t,r)=>t in e?me(e,t,{enumerable:!0,configurable:!0,writable:!0,value:r}):e[t]=r,B=(e,t)=>{for(var r in t||(t={}))tt.call(t,r)&&rt(e,r,t[r]);if(et)for(var r of et(t))Tr.call(t,r)&&rt(e,r,t[r]);return e},Q=(e,t)=>Sr(e,Hr(t)),Lr=e=>me(e,"__esModule",{value:!0});var K=(e,t)=>()=>(t||e((t={exports:{}}).exports,t),t.exports),Or=(e,t)=>{for(var r in t)me(e,r,{get:t[r],enumerable:!0})},Nr=(e,t,r)=>{if(t&&typeof t=="object"||typeof t=="function")for(let n of $r(t))!tt.call(e,n)&&n!=="default"&&me(e,n,{get:()=>t[n],enumerable:!(r=vr(t,n))||r.enumerable});return e},X=e=>Nr(Lr(me(e!=null?wr(kr(e)):{},"default",e&&e.__esModule&&"default"in e?{get:()=>e.default,enumerable:!0}:{value:e,enumerable:!0})),e);var $e=K(te=>{"use strict";te.isInteger=e=>typeof e=="number"?Number.isInteger(e):typeof e=="string"&&e.trim()!==""?Number.isInteger(Number(e)):!1;te.find=(e,t)=>e.nodes.find(r=>r.type===t);te.exceedsLimit=(e,t,r=1,n)=>n===!1||!te.isInteger(e)||!te.isInteger(t)?!1:(Number(t)-Number(e))/Number(r)>=n;te.escapeNode=(e,t=0,r)=>{let n=e.nodes[t];!n||(r&&n.type===r||n.type==="open"||n.type==="close")&&n.escaped!==!0&&(n.value="\\"+n.value,n.escaped=!0)};te.encloseBrace=e=>e.type!=="brace"?!1:e.commas>>0+e.ranges>>0==0?(e.invalid=!0,!0):!1;te.isInvalidBrace=e=>e.type!=="brace"?!1:e.invalid===!0||e.dollar?!0:e.commas>>0+e.ranges>>0==0||e.open!==!0||e.close!==!0?(e.invalid=!0,!0):!1;te.isOpenOrClose=e=>e.type==="open"||e.type==="close"?!0:e.open===!0||e.close===!0;te.reduce=e=>e.reduce((t,r)=>(r.type==="text"&&t.push(r.value),r.type==="range"&&(r.type="text"),t),[]);te.flatten=(...e)=>{let t=[],r=n=>{for(let s=0;s{"use strict";var it=$e();at.exports=(e,t={})=>{let r=(n,s={})=>{let a=t.escapeInvalid&&it.isInvalidBrace(s),i=n.invalid===!0&&t.escapeInvalid===!0,o="";if(n.value)return(a||i)&&it.isOpenOrClose(n)?"\\"+n.value:n.value;if(n.value)return n.value;if(n.nodes)for(let h of n.nodes)o+=r(h);return o};return r(e)}});var ct=K((os,ot)=>{"use strict";ot.exports=function(e){return typeof e=="number"?e-e==0:typeof e=="string"&&e.trim()!==""?Number.isFinite?Number.isFinite(+e):isFinite(+e):!1}});var At=K((cs,ut)=>{"use strict";var lt=ct(),pe=(e,t,r)=>{if(lt(e)===!1)throw new TypeError("toRegexRange: expected the first argument to be a number");if(t===void 0||e===t)return String(e);if(lt(t)===!1)throw new TypeError("toRegexRange: expected the second argument to be a number.");let n=B({relaxZeros:!0},r);typeof n.strictZeros=="boolean"&&(n.relaxZeros=n.strictZeros===!1);let s=String(n.relaxZeros),a=String(n.shorthand),i=String(n.capture),o=String(n.wrap),h=e+":"+t+"="+s+a+i+o;if(pe.cache.hasOwnProperty(h))return pe.cache[h].result;let g=Math.min(e,t),f=Math.max(e,t);if(Math.abs(g-f)===1){let R=e+"|"+t;return n.capture?`(${R})`:n.wrap===!1?R:`(?:${R})`}let A=ft(e)||ft(t),p={min:e,max:t,a:g,b:f},k=[],y=[];if(A&&(p.isPadded=A,p.maxLen=String(p.max).length),g<0){let R=f<0?Math.abs(f):1;y=pt(R,Math.abs(g),p,n),g=p.a=0}return f>=0&&(k=pt(g,f,p,n)),p.negatives=y,p.positives=k,p.result=Ir(y,k,n),n.capture===!0?p.result=`(${p.result})`:n.wrap!==!1&&k.length+y.length>1&&(p.result=`(?:${p.result})`),pe.cache[h]=p,p.result};function Ir(e,t,r){let n=Pe(e,t,"-",!1,r)||[],s=Pe(t,e,"",!1,r)||[],a=Pe(e,t,"-?",!0,r)||[];return n.concat(a).concat(s).join("|")}function Mr(e,t){let r=1,n=1,s=ht(e,r),a=new Set([t]);for(;e<=s&&s<=t;)a.add(s),r+=1,s=ht(e,r);for(s=dt(t+1,n)-1;e1&&o.count.pop(),o.count.push(f.count[0]),o.string=o.pattern+gt(o.count),i=g+1;continue}r.isPadded&&(A=Gr(g,r,n)),f.string=A+f.pattern+gt(f.count),a.push(f),i=g+1,o=f}return a}function Pe(e,t,r,n,s){let a=[];for(let i of e){let{string:o}=i;!n&&!mt(t,"string",o)&&a.push(r+o),n&&mt(t,"string",o)&&a.push(r+o)}return a}function Pr(e,t){let r=[];for(let n=0;nt?1:t>e?-1:0}function mt(e,t,r){return e.some(n=>n[t]===r)}function ht(e,t){return Number(String(e).slice(0,-t)+"9".repeat(t))}function dt(e,t){return e-e%Math.pow(10,t)}function gt(e){let[t=0,r=""]=e;return r||t>1?`{${t+(r?","+r:"")}}`:""}function Dr(e,t,r){return`[${e}${t-e==1?"":"-"}${t}]`}function ft(e){return/^-?(0+)\d/.test(e)}function Gr(e,t,r){if(!t.isPadded)return e;let n=Math.abs(t.maxLen-String(e).length),s=r.relaxZeros!==!1;switch(n){case 0:return"";case 1:return s?"0?":"0";case 2:return s?"0{0,2}":"00";default:return s?`0{0,${n}}`:`0{${n}}`}}pe.cache={};pe.clearCache=()=>pe.cache={};ut.exports=pe});var Ge=K((us,Rt)=>{"use strict";var qr=require("util"),yt=At(),bt=e=>e!==null&&typeof e=="object"&&!Array.isArray(e),Kr=e=>t=>e===!0?Number(t):String(t),De=e=>typeof e=="number"||typeof e=="string"&&e!=="",Re=e=>Number.isInteger(+e),Ue=e=>{let t=`${e}`,r=-1;if(t[0]==="-"&&(t=t.slice(1)),t==="0")return!1;for(;t[++r]==="0";);return r>0},Wr=(e,t,r)=>typeof e=="string"||typeof t=="string"?!0:r.stringify===!0,jr=(e,t,r)=>{if(t>0){let n=e[0]==="-"?"-":"";n&&(e=e.slice(1)),e=n+e.padStart(n?t-1:t,"0")}return r===!1?String(e):e},_t=(e,t)=>{let r=e[0]==="-"?"-":"";for(r&&(e=e.slice(1),t--);e.length{e.negatives.sort((i,o)=>io?1:0),e.positives.sort((i,o)=>io?1:0);let r=t.capture?"":"?:",n="",s="",a;return e.positives.length&&(n=e.positives.join("|")),e.negatives.length&&(s=`-(${r}${e.negatives.join("|")})`),n&&s?a=`${n}|${s}`:a=n||s,t.wrap?`(${r}${a})`:a},Et=(e,t,r,n)=>{if(r)return yt(e,t,B({wrap:!1},n));let s=String.fromCharCode(e);if(e===t)return s;let a=String.fromCharCode(t);return`[${s}-${a}]`},xt=(e,t,r)=>{if(Array.isArray(e)){let n=r.wrap===!0,s=r.capture?"":"?:";return n?`(${s}${e.join("|")})`:e.join("|")}return yt(e,t,r)},Ct=(...e)=>new RangeError("Invalid range arguments: "+qr.inspect(...e)),wt=(e,t,r)=>{if(r.strictRanges===!0)throw Ct([e,t]);return[]},Qr=(e,t)=>{if(t.strictRanges===!0)throw new TypeError(`Expected step "${e}" to be a number`);return[]},Xr=(e,t,r=1,n={})=>{let s=Number(e),a=Number(t);if(!Number.isInteger(s)||!Number.isInteger(a)){if(n.strictRanges===!0)throw Ct([e,t]);return[]}s===0&&(s=0),a===0&&(a=0);let i=s>a,o=String(e),h=String(t),g=String(r);r=Math.max(Math.abs(r),1);let f=Ue(o)||Ue(h)||Ue(g),A=f?Math.max(o.length,h.length,g.length):0,p=f===!1&&Wr(e,t,n)===!1,k=n.transform||Kr(p);if(n.toRegex&&r===1)return Et(_t(e,A),_t(t,A),!0,n);let y={negatives:[],positives:[]},R=T=>y[T<0?"negatives":"positives"].push(Math.abs(T)),_=[],x=0;for(;i?s>=a:s<=a;)n.toRegex===!0&&r>1?R(s):_.push(jr(k(s,x),A,p)),s=i?s-r:s+r,x++;return n.toRegex===!0?r>1?Fr(y,n):xt(_,null,B({wrap:!1},n)):_},Zr=(e,t,r=1,n={})=>{if(!Re(e)&&e.length>1||!Re(t)&&t.length>1)return wt(e,t,n);let s=n.transform||(p=>String.fromCharCode(p)),a=`${e}`.charCodeAt(0),i=`${t}`.charCodeAt(0),o=a>i,h=Math.min(a,i),g=Math.max(a,i);if(n.toRegex&&r===1)return Et(h,g,!1,n);let f=[],A=0;for(;o?a>=i:a<=i;)f.push(s(a,A)),a=o?a-r:a+r,A++;return n.toRegex===!0?xt(f,null,{wrap:!1,options:n}):f},Te=(e,t,r,n={})=>{if(t==null&&De(e))return[e];if(!De(e)||!De(t))return wt(e,t,n);if(typeof r=="function")return Te(e,t,1,{transform:r});if(bt(r))return Te(e,t,0,r);let s=B({},n);return s.capture===!0&&(s.wrap=!0),r=r||s.step||1,Re(r)?Re(e)&&Re(t)?Xr(e,t,r,s):Zr(e,t,Math.max(Math.abs(r),1),s):r!=null&&!bt(r)?Qr(r,s):Te(e,t,1,r)};Rt.exports=Te});var Ht=K((ls,St)=>{"use strict";var Yr=Ge(),vt=$e(),zr=(e,t={})=>{let r=(n,s={})=>{let a=vt.isInvalidBrace(s),i=n.invalid===!0&&t.escapeInvalid===!0,o=a===!0||i===!0,h=t.escapeInvalid===!0?"\\":"",g="";if(n.isOpen===!0||n.isClose===!0)return h+n.value;if(n.type==="open")return o?h+n.value:"(";if(n.type==="close")return o?h+n.value:")";if(n.type==="comma")return n.prev.type==="comma"?"":o?n.value:"|";if(n.value)return n.value;if(n.nodes&&n.ranges>0){let f=vt.reduce(n.nodes),A=Yr(...f,Q(B({},t),{wrap:!1,toRegex:!0}));if(A.length!==0)return f.length>1&&A.length>1?`(${A})`:A}if(n.nodes)for(let f of n.nodes)g+=r(f,n);return g};return r(e)};St.exports=zr});var Tt=K((ps,$t)=>{"use strict";var Vr=Ge(),kt=ke(),he=$e(),fe=(e="",t="",r=!1)=>{let n=[];if(e=[].concat(e),t=[].concat(t),!t.length)return e;if(!e.length)return r?he.flatten(t).map(s=>`{${s}}`):t;for(let s of e)if(Array.isArray(s))for(let a of s)n.push(fe(a,t,r));else for(let a of t)r===!0&&typeof a=="string"&&(a=`{${a}}`),n.push(Array.isArray(a)?fe(s,a,r):s+a);return he.flatten(n)},Jr=(e,t={})=>{let r=t.rangeLimit===void 0?1e3:t.rangeLimit,n=(s,a={})=>{s.queue=[];let i=a,o=a.queue;for(;i.type!=="brace"&&i.type!=="root"&&i.parent;)i=i.parent,o=i.queue;if(s.invalid||s.dollar){o.push(fe(o.pop(),kt(s,t)));return}if(s.type==="brace"&&s.invalid!==!0&&s.nodes.length===2){o.push(fe(o.pop(),["{}"]));return}if(s.nodes&&s.ranges>0){let A=he.reduce(s.nodes);if(he.exceedsLimit(...A,t.step,r))throw new RangeError("expanded array length exceeds range limit. Use options.rangeLimit to increase or disable the limit.");let p=Vr(...A,t);p.length===0&&(p=kt(s,t)),o.push(fe(o.pop(),p)),s.nodes=[];return}let h=he.encloseBrace(s),g=s.queue,f=s;for(;f.type!=="brace"&&f.type!=="root"&&f.parent;)f=f.parent,g=f.queue;for(let A=0;A{"use strict";Lt.exports={MAX_LENGTH:1024*64,CHAR_0:"0",CHAR_9:"9",CHAR_UPPERCASE_A:"A",CHAR_LOWERCASE_A:"a",CHAR_UPPERCASE_Z:"Z",CHAR_LOWERCASE_Z:"z",CHAR_LEFT_PARENTHESES:"(",CHAR_RIGHT_PARENTHESES:")",CHAR_ASTERISK:"*",CHAR_AMPERSAND:"&",CHAR_AT:"@",CHAR_BACKSLASH:"\\",CHAR_BACKTICK:"`",CHAR_CARRIAGE_RETURN:"\r",CHAR_CIRCUMFLEX_ACCENT:"^",CHAR_COLON:":",CHAR_COMMA:",",CHAR_DOLLAR:"$",CHAR_DOT:".",CHAR_DOUBLE_QUOTE:'"',CHAR_EQUAL:"=",CHAR_EXCLAMATION_MARK:"!",CHAR_FORM_FEED:"\f",CHAR_FORWARD_SLASH:"/",CHAR_HASH:"#",CHAR_HYPHEN_MINUS:"-",CHAR_LEFT_ANGLE_BRACKET:"<",CHAR_LEFT_CURLY_BRACE:"{",CHAR_LEFT_SQUARE_BRACKET:"[",CHAR_LINE_FEED:` 7 | `,CHAR_NO_BREAK_SPACE:"\xA0",CHAR_PERCENT:"%",CHAR_PLUS:"+",CHAR_QUESTION_MARK:"?",CHAR_RIGHT_ANGLE_BRACKET:">",CHAR_RIGHT_CURLY_BRACE:"}",CHAR_RIGHT_SQUARE_BRACKET:"]",CHAR_SEMICOLON:";",CHAR_SINGLE_QUOTE:"'",CHAR_SPACE:" ",CHAR_TAB:" ",CHAR_UNDERSCORE:"_",CHAR_VERTICAL_LINE:"|",CHAR_ZERO_WIDTH_NOBREAK_SPACE:"\uFEFF"}});var Pt=K((hs,Nt)=>{"use strict";var en=ke(),{MAX_LENGTH:It,CHAR_BACKSLASH:qe,CHAR_BACKTICK:tn,CHAR_COMMA:rn,CHAR_DOT:nn,CHAR_LEFT_PARENTHESES:sn,CHAR_RIGHT_PARENTHESES:an,CHAR_LEFT_CURLY_BRACE:on,CHAR_RIGHT_CURLY_BRACE:cn,CHAR_LEFT_SQUARE_BRACKET:Bt,CHAR_RIGHT_SQUARE_BRACKET:Mt,CHAR_DOUBLE_QUOTE:un,CHAR_SINGLE_QUOTE:ln,CHAR_NO_BREAK_SPACE:pn,CHAR_ZERO_WIDTH_NOBREAK_SPACE:fn}=Ot(),hn=(e,t={})=>{if(typeof e!="string")throw new TypeError("Expected a string");let r=t||{},n=typeof r.maxLength=="number"?Math.min(It,r.maxLength):It;if(e.length>n)throw new SyntaxError(`Input length (${e.length}), exceeds max characters (${n})`);let s={type:"root",input:e,nodes:[]},a=[s],i=s,o=s,h=0,g=e.length,f=0,A=0,p,k={},y=()=>e[f++],R=_=>{if(_.type==="text"&&o.type==="dot"&&(o.type="text"),o&&o.type==="text"&&_.type==="text"){o.value+=_.value;return}return i.nodes.push(_),_.parent=i,_.prev=o,o=_,_};for(R({type:"bos"});f0){if(i.ranges>0){i.ranges=0;let _=i.nodes.shift();i.nodes=[_,{type:"text",value:en(i)}]}R({type:"comma",value:p}),i.commas++;continue}if(p===nn&&A>0&&i.commas===0){let _=i.nodes;if(A===0||_.length===0){R({type:"text",value:p});continue}if(o.type==="dot"){if(i.range=[],o.value+=p,o.type="range",i.nodes.length!==3&&i.nodes.length!==5){i.invalid=!0,i.ranges=0,o.type="text";continue}i.ranges++,i.args=[];continue}if(o.type==="range"){_.pop();let x=_[_.length-1];x.value+=o.value+p,o=x,i.ranges--;continue}R({type:"dot",value:p});continue}R({type:"text",value:p})}do if(i=a.pop(),i.type!=="root"){i.nodes.forEach(T=>{T.nodes||(T.type==="open"&&(T.isOpen=!0),T.type==="close"&&(T.isClose=!0),T.nodes||(T.type="text"),T.invalid=!0)});let _=a[a.length-1],x=_.nodes.indexOf(i);_.nodes.splice(x,1,...i.nodes)}while(a.length>0);return R({type:"eos"}),s};Nt.exports=hn});var Gt=K((ds,Dt)=>{"use strict";var Ut=ke(),dn=Ht(),gn=Tt(),mn=Pt(),V=(e,t={})=>{let r=[];if(Array.isArray(e))for(let n of e){let s=V.create(n,t);Array.isArray(s)?r.push(...s):r.push(s)}else r=[].concat(V.create(e,t));return t&&t.expand===!0&&t.nodupes===!0&&(r=[...new Set(r)]),r};V.parse=(e,t={})=>mn(e,t);V.stringify=(e,t={})=>typeof e=="string"?Ut(V.parse(e,t),t):Ut(e,t);V.compile=(e,t={})=>(typeof e=="string"&&(e=V.parse(e,t)),dn(e,t));V.expand=(e,t={})=>{typeof e=="string"&&(e=V.parse(e,t));let r=gn(e,t);return t.noempty===!0&&(r=r.filter(Boolean)),t.nodupes===!0&&(r=[...new Set(r)]),r};V.create=(e,t={})=>e===""||e.length<3?[e]:t.expand!==!0?V.compile(e,t):V.expand(e,t);Dt.exports=V});var ye=K((gs,qt)=>{"use strict";var An=require("path"),ie="\\\\/",Kt=`[^${ie}]`,ce="\\.",Rn="\\+",yn="\\?",Le="\\/",bn="(?=.)",Wt="[^/]",Ke=`(?:${Le}|$)`,jt=`(?:^|${Le})`,We=`${ce}{1,2}${Ke}`,_n=`(?!${ce})`,En=`(?!${jt}${We})`,xn=`(?!${ce}{0,1}${Ke})`,Cn=`(?!${We})`,wn=`[^.${Le}]`,Sn=`${Wt}*?`,Ft={DOT_LITERAL:ce,PLUS_LITERAL:Rn,QMARK_LITERAL:yn,SLASH_LITERAL:Le,ONE_CHAR:bn,QMARK:Wt,END_ANCHOR:Ke,DOTS_SLASH:We,NO_DOT:_n,NO_DOTS:En,NO_DOT_SLASH:xn,NO_DOTS_SLASH:Cn,QMARK_NO_DOT:wn,STAR:Sn,START_ANCHOR:jt},vn=Q(B({},Ft),{SLASH_LITERAL:`[${ie}]`,QMARK:Kt,STAR:`${Kt}*?`,DOTS_SLASH:`${ce}{1,2}(?:[${ie}]|$)`,NO_DOT:`(?!${ce})`,NO_DOTS:`(?!(?:^|[${ie}])${ce}{1,2}(?:[${ie}]|$))`,NO_DOT_SLASH:`(?!${ce}{0,1}(?:[${ie}]|$))`,NO_DOTS_SLASH:`(?!${ce}{1,2}(?:[${ie}]|$))`,QMARK_NO_DOT:`[^.${ie}]`,START_ANCHOR:`(?:^|[${ie}])`,END_ANCHOR:`(?:[${ie}]|$)`}),Hn={alnum:"a-zA-Z0-9",alpha:"a-zA-Z",ascii:"\\x00-\\x7F",blank:" \\t",cntrl:"\\x00-\\x1F\\x7F",digit:"0-9",graph:"\\x21-\\x7E",lower:"a-z",print:"\\x20-\\x7E ",punct:"\\-!\"#$%&'()\\*+,./:;<=>?@[\\]^_`{|}~",space:" \\t\\r\\n\\v\\f",upper:"A-Z",word:"A-Za-z0-9_",xdigit:"A-Fa-f0-9"};qt.exports={MAX_LENGTH:1024*64,POSIX_REGEX_SOURCE:Hn,REGEX_BACKSLASH:/\\(?![*+?^${}(|)[\]])/g,REGEX_NON_SPECIAL_CHARS:/^[^@![\].,$*+?^{}()|\\/]+/,REGEX_SPECIAL_CHARS:/[-*+?.^${}(|)[\]]/,REGEX_SPECIAL_CHARS_BACKREF:/(\\?)((\W)(\3*))/g,REGEX_SPECIAL_CHARS_GLOBAL:/([-*+?.^${}(|)[\]])/g,REGEX_REMOVE_BACKSLASH:/(?:\[.*?[^\\]\]|\\(?=.))/g,REPLACEMENTS:{"***":"*","**/**":"**","**/**/**":"**"},CHAR_0:48,CHAR_9:57,CHAR_UPPERCASE_A:65,CHAR_LOWERCASE_A:97,CHAR_UPPERCASE_Z:90,CHAR_LOWERCASE_Z:122,CHAR_LEFT_PARENTHESES:40,CHAR_RIGHT_PARENTHESES:41,CHAR_ASTERISK:42,CHAR_AMPERSAND:38,CHAR_AT:64,CHAR_BACKWARD_SLASH:92,CHAR_CARRIAGE_RETURN:13,CHAR_CIRCUMFLEX_ACCENT:94,CHAR_COLON:58,CHAR_COMMA:44,CHAR_DOT:46,CHAR_DOUBLE_QUOTE:34,CHAR_EQUAL:61,CHAR_EXCLAMATION_MARK:33,CHAR_FORM_FEED:12,CHAR_FORWARD_SLASH:47,CHAR_GRAVE_ACCENT:96,CHAR_HASH:35,CHAR_HYPHEN_MINUS:45,CHAR_LEFT_ANGLE_BRACKET:60,CHAR_LEFT_CURLY_BRACE:123,CHAR_LEFT_SQUARE_BRACKET:91,CHAR_LINE_FEED:10,CHAR_NO_BREAK_SPACE:160,CHAR_PERCENT:37,CHAR_PLUS:43,CHAR_QUESTION_MARK:63,CHAR_RIGHT_ANGLE_BRACKET:62,CHAR_RIGHT_CURLY_BRACE:125,CHAR_RIGHT_SQUARE_BRACKET:93,CHAR_SEMICOLON:59,CHAR_SINGLE_QUOTE:39,CHAR_SPACE:32,CHAR_TAB:9,CHAR_UNDERSCORE:95,CHAR_VERTICAL_LINE:124,CHAR_ZERO_WIDTH_NOBREAK_SPACE:65279,SEP:An.sep,extglobChars(e){return{"!":{type:"negate",open:"(?:(?!(?:",close:`))${e.STAR})`},"?":{type:"qmark",open:"(?:",close:")?"},"+":{type:"plus",open:"(?:",close:")+"},"*":{type:"star",open:"(?:",close:")*"},"@":{type:"at",open:"(?:",close:")"}}},globChars(e){return e===!0?vn:Ft}}});var be=K(Z=>{"use strict";var $n=require("path"),kn=process.platform==="win32",{REGEX_BACKSLASH:Tn,REGEX_REMOVE_BACKSLASH:Ln,REGEX_SPECIAL_CHARS:On,REGEX_SPECIAL_CHARS_GLOBAL:Nn}=ye();Z.isObject=e=>e!==null&&typeof e=="object"&&!Array.isArray(e);Z.hasRegexChars=e=>On.test(e);Z.isRegexChar=e=>e.length===1&&Z.hasRegexChars(e);Z.escapeRegex=e=>e.replace(Nn,"\\$1");Z.toPosixSlashes=e=>e.replace(Tn,"/");Z.removeBackslashes=e=>e.replace(Ln,t=>t==="\\"?"":t);Z.supportsLookbehinds=()=>{let e=process.version.slice(1).split(".").map(Number);return e.length===3&&e[0]>=9||e[0]===8&&e[1]>=10};Z.isWindows=e=>e&&typeof e.windows=="boolean"?e.windows:kn===!0||$n.sep==="\\";Z.escapeLast=(e,t,r)=>{let n=e.lastIndexOf(t,r);return n===-1?e:e[n-1]==="\\"?Z.escapeLast(e,t,n-1):`${e.slice(0,n)}\\${e.slice(n)}`};Z.removePrefix=(e,t={})=>{let r=e;return r.startsWith("./")&&(r=r.slice(2),t.prefix="./"),r};Z.wrapOutput=(e,t={},r={})=>{let n=r.contains?"":"^",s=r.contains?"":"$",a=`${n}(?:${e})${s}`;return t.negated===!0&&(a=`(?:^(?!${a}).*$)`),a}});var er=K((As,Qt)=>{"use strict";var Xt=be(),{CHAR_ASTERISK:je,CHAR_AT:In,CHAR_BACKWARD_SLASH:_e,CHAR_COMMA:Bn,CHAR_DOT:Fe,CHAR_EXCLAMATION_MARK:Qe,CHAR_FORWARD_SLASH:Zt,CHAR_LEFT_CURLY_BRACE:Xe,CHAR_LEFT_PARENTHESES:Ze,CHAR_LEFT_SQUARE_BRACKET:Mn,CHAR_PLUS:Pn,CHAR_QUESTION_MARK:Yt,CHAR_RIGHT_CURLY_BRACE:Dn,CHAR_RIGHT_PARENTHESES:zt,CHAR_RIGHT_SQUARE_BRACKET:Un}=ye(),Vt=e=>e===Zt||e===_e,Jt=e=>{e.isPrefix!==!0&&(e.depth=e.isGlobstar?Infinity:1)},Gn=(e,t)=>{let r=t||{},n=e.length-1,s=r.parts===!0||r.scanToEnd===!0,a=[],i=[],o=[],h=e,g=-1,f=0,A=0,p=!1,k=!1,y=!1,R=!1,_=!1,x=!1,T=!1,O=!1,W=!1,G=!1,ne=0,E,b,C={value:"",depth:0,isGlob:!1},M=()=>g>=n,l=()=>h.charCodeAt(g+1),H=()=>(E=b,h.charCodeAt(++g));for(;g0&&(j=h.slice(0,f),h=h.slice(f),A-=f),w&&y===!0&&A>0?(w=h.slice(0,A),c=h.slice(A)):y===!0?(w="",c=h):w=h,w&&w!==""&&w!=="/"&&w!==h&&Vt(w.charCodeAt(w.length-1))&&(w=w.slice(0,-1)),r.unescape===!0&&(c&&(c=Xt.removeBackslashes(c)),w&&T===!0&&(w=Xt.removeBackslashes(w)));let u={prefix:j,input:e,start:f,base:w,glob:c,isBrace:p,isBracket:k,isGlob:y,isExtglob:R,isGlobstar:_,negated:O,negatedExtglob:W};if(r.tokens===!0&&(u.maxDepth=0,Vt(b)||i.push(C),u.tokens=i),r.parts===!0||r.tokens===!0){let I;for(let $=0;${"use strict";var Oe=ye(),J=be(),{MAX_LENGTH:Ne,POSIX_REGEX_SOURCE:qn,REGEX_NON_SPECIAL_CHARS:Kn,REGEX_SPECIAL_CHARS_BACKREF:Wn,REPLACEMENTS:rr}=Oe,jn=(e,t)=>{if(typeof t.expandRange=="function")return t.expandRange(...e,t);e.sort();let r=`[${e.join("-")}]`;try{new RegExp(r)}catch(n){return e.map(s=>J.escapeRegex(s)).join("..")}return r},de=(e,t)=>`Missing ${e}: "${t}" - use "\\\\${t}" to match literal characters`,nr=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");e=rr[e]||e;let r=B({},t),n=typeof r.maxLength=="number"?Math.min(Ne,r.maxLength):Ne,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);let a={type:"bos",value:"",output:r.prepend||""},i=[a],o=r.capture?"":"?:",h=J.isWindows(t),g=Oe.globChars(h),f=Oe.extglobChars(g),{DOT_LITERAL:A,PLUS_LITERAL:p,SLASH_LITERAL:k,ONE_CHAR:y,DOTS_SLASH:R,NO_DOT:_,NO_DOT_SLASH:x,NO_DOTS_SLASH:T,QMARK:O,QMARK_NO_DOT:W,STAR:G,START_ANCHOR:ne}=g,E=m=>`(${o}(?:(?!${ne}${m.dot?R:A}).)*?)`,b=r.dot?"":_,C=r.dot?O:W,M=r.bash===!0?E(r):G;r.capture&&(M=`(${M})`),typeof r.noext=="boolean"&&(r.noextglob=r.noext);let l={input:e,index:-1,start:0,dot:r.dot===!0,consumed:"",output:"",prefix:"",backtrack:!1,negated:!1,brackets:0,braces:0,parens:0,quotes:0,globstar:!1,tokens:i};e=J.removePrefix(e,l),s=e.length;let H=[],w=[],j=[],c=a,u,I=()=>l.index===s-1,$=l.peek=(m=1)=>e[l.index+m],ee=l.advance=()=>e[++l.index]||"",se=()=>e.slice(l.index+1),z=(m="",L=0)=>{l.consumed+=m,l.index+=L},Ce=m=>{l.output+=m.output!=null?m.output:m.value,z(m.value)},xr=()=>{let m=1;for(;$()==="!"&&($(2)!=="("||$(3)==="?");)ee(),l.start++,m++;return m%2==0?!1:(l.negated=!0,l.start++,!0)},we=m=>{l[m]++,j.push(m)},ue=m=>{l[m]--,j.pop()},v=m=>{if(c.type==="globstar"){let L=l.braces>0&&(m.type==="comma"||m.type==="brace"),d=m.extglob===!0||H.length&&(m.type==="pipe"||m.type==="paren");m.type!=="slash"&&m.type!=="paren"&&!L&&!d&&(l.output=l.output.slice(0,-c.output.length),c.type="star",c.value="*",c.output=M,l.output+=c.output)}if(H.length&&m.type!=="paren"&&(H[H.length-1].inner+=m.value),(m.value||m.output)&&Ce(m),c&&c.type==="text"&&m.type==="text"){c.value+=m.value,c.output=(c.output||"")+m.value;return}m.prev=c,i.push(m),c=m},Se=(m,L)=>{let d=Q(B({},f[L]),{conditions:1,inner:""});d.prev=c,d.parens=l.parens,d.output=l.output;let S=(r.capture?"(":"")+d.open;we("parens"),v({type:m,value:L,output:l.output?"":y}),v({type:"paren",extglob:!0,value:ee(),output:S}),H.push(d)},Cr=m=>{let L=m.close+(r.capture?")":""),d;if(m.type==="negate"){let S=M;m.inner&&m.inner.length>1&&m.inner.includes("/")&&(S=E(r)),(S!==M||I()||/^\)+$/.test(se()))&&(L=m.close=`)$))${S}`),m.inner.includes("*")&&(d=se())&&/^\.[^\\/.]+$/.test(d)&&(L=m.close=`)${d})${S})`),m.prev.type==="bos"&&(l.negatedExtglob=!0)}v({type:"paren",extglob:!0,value:u,output:L}),ue("parens")};if(r.fastpaths!==!1&&!/(^[*!]|[/()[\]{}"])/.test(e)){let m=!1,L=e.replace(Wn,(d,S,P,F,q,Me)=>F==="\\"?(m=!0,d):F==="?"?S?S+F+(q?O.repeat(q.length):""):Me===0?C+(q?O.repeat(q.length):""):O.repeat(P.length):F==="."?A.repeat(P.length):F==="*"?S?S+F+(q?M:""):M:S?d:`\\${d}`);return m===!0&&(r.unescape===!0?L=L.replace(/\\/g,""):L=L.replace(/\\+/g,d=>d.length%2==0?"\\\\":d?"\\":"")),L===e&&r.contains===!0?(l.output=e,l):(l.output=J.wrapOutput(L,l,t),l)}for(;!I();){if(u=ee(),u==="\0")continue;if(u==="\\"){let d=$();if(d==="/"&&r.bash!==!0||d==="."||d===";")continue;if(!d){u+="\\",v({type:"text",value:u});continue}let S=/^\\+/.exec(se()),P=0;if(S&&S[0].length>2&&(P=S[0].length,l.index+=P,P%2!=0&&(u+="\\")),r.unescape===!0?u=ee():u+=ee(),l.brackets===0){v({type:"text",value:u});continue}}if(l.brackets>0&&(u!=="]"||c.value==="["||c.value==="[^")){if(r.posix!==!1&&u===":"){let d=c.value.slice(1);if(d.includes("[")&&(c.posix=!0,d.includes(":"))){let S=c.value.lastIndexOf("["),P=c.value.slice(0,S),F=c.value.slice(S+2),q=qn[F];if(q){c.value=P+q,l.backtrack=!0,ee(),!a.output&&i.indexOf(c)===1&&(a.output=y);continue}}}(u==="["&&$()!==":"||u==="-"&&$()==="]")&&(u=`\\${u}`),u==="]"&&(c.value==="["||c.value==="[^")&&(u=`\\${u}`),r.posix===!0&&u==="!"&&c.value==="["&&(u="^"),c.value+=u,Ce({value:u});continue}if(l.quotes===1&&u!=='"'){u=J.escapeRegex(u),c.value+=u,Ce({value:u});continue}if(u==='"'){l.quotes=l.quotes===1?0:1,r.keepQuotes===!0&&v({type:"text",value:u});continue}if(u==="("){we("parens"),v({type:"paren",value:u});continue}if(u===")"){if(l.parens===0&&r.strictBrackets===!0)throw new SyntaxError(de("opening","("));let d=H[H.length-1];if(d&&l.parens===d.parens+1){Cr(H.pop());continue}v({type:"paren",value:u,output:l.parens?")":"\\)"}),ue("parens");continue}if(u==="["){if(r.nobracket===!0||!se().includes("]")){if(r.nobracket!==!0&&r.strictBrackets===!0)throw new SyntaxError(de("closing","]"));u=`\\${u}`}else we("brackets");v({type:"bracket",value:u});continue}if(u==="]"){if(r.nobracket===!0||c&&c.type==="bracket"&&c.value.length===1){v({type:"text",value:u,output:`\\${u}`});continue}if(l.brackets===0){if(r.strictBrackets===!0)throw new SyntaxError(de("opening","["));v({type:"text",value:u,output:`\\${u}`});continue}ue("brackets");let d=c.value.slice(1);if(c.posix!==!0&&d[0]==="^"&&!d.includes("/")&&(u=`/${u}`),c.value+=u,Ce({value:u}),r.literalBrackets===!1||J.hasRegexChars(d))continue;let S=J.escapeRegex(c.value);if(l.output=l.output.slice(0,-c.value.length),r.literalBrackets===!0){l.output+=S,c.value=S;continue}c.value=`(${o}${S}|${c.value})`,l.output+=c.value;continue}if(u==="{"&&r.nobrace!==!0){we("braces");let d={type:"brace",value:u,output:"(",outputIndex:l.output.length,tokensIndex:l.tokens.length};w.push(d),v(d);continue}if(u==="}"){let d=w[w.length-1];if(r.nobrace===!0||!d){v({type:"text",value:u,output:u});continue}let S=")";if(d.dots===!0){let P=i.slice(),F=[];for(let q=P.length-1;q>=0&&(i.pop(),P[q].type!=="brace");q--)P[q].type!=="dots"&&F.unshift(P[q].value);S=jn(F,r),l.backtrack=!0}if(d.comma!==!0&&d.dots!==!0){let P=l.output.slice(0,d.outputIndex),F=l.tokens.slice(d.tokensIndex);d.value=d.output="\\{",u=S="\\}",l.output=P;for(let q of F)l.output+=q.output||q.value}v({type:"brace",value:u,output:S}),ue("braces"),w.pop();continue}if(u==="|"){H.length>0&&H[H.length-1].conditions++,v({type:"text",value:u});continue}if(u===","){let d=u,S=w[w.length-1];S&&j[j.length-1]==="braces"&&(S.comma=!0,d="|"),v({type:"comma",value:u,output:d});continue}if(u==="/"){if(c.type==="dot"&&l.index===l.start+1){l.start=l.index+1,l.consumed="",l.output="",i.pop(),c=a;continue}v({type:"slash",value:u,output:k});continue}if(u==="."){if(l.braces>0&&c.type==="dot"){c.value==="."&&(c.output=A);let d=w[w.length-1];c.type="dots",c.output+=u,c.value+=u,d.dots=!0;continue}if(l.braces+l.parens===0&&c.type!=="bos"&&c.type!=="slash"){v({type:"text",value:u,output:A});continue}v({type:"dot",value:u,output:A});continue}if(u==="?"){if(!(c&&c.value==="(")&&r.noextglob!==!0&&$()==="("&&$(2)!=="?"){Se("qmark",u);continue}if(c&&c.type==="paren"){let S=$(),P=u;if(S==="<"&&!J.supportsLookbehinds())throw new Error("Node.js v10 or higher is required for regex lookbehinds");(c.value==="("&&!/[!=<:]/.test(S)||S==="<"&&!/<([!=]|\w+>)/.test(se()))&&(P=`\\${u}`),v({type:"text",value:u,output:P});continue}if(r.dot!==!0&&(c.type==="slash"||c.type==="bos")){v({type:"qmark",value:u,output:W});continue}v({type:"qmark",value:u,output:O});continue}if(u==="!"){if(r.noextglob!==!0&&$()==="("&&($(2)!=="?"||!/[!=<:]/.test($(3)))){Se("negate",u);continue}if(r.nonegate!==!0&&l.index===0){xr();continue}}if(u==="+"){if(r.noextglob!==!0&&$()==="("&&$(2)!=="?"){Se("plus",u);continue}if(c&&c.value==="("||r.regex===!1){v({type:"plus",value:u,output:p});continue}if(c&&(c.type==="bracket"||c.type==="paren"||c.type==="brace")||l.parens>0){v({type:"plus",value:u});continue}v({type:"plus",value:p});continue}if(u==="@"){if(r.noextglob!==!0&&$()==="("&&$(2)!=="?"){v({type:"at",extglob:!0,value:u,output:""});continue}v({type:"text",value:u});continue}if(u!=="*"){(u==="$"||u==="^")&&(u=`\\${u}`);let d=Kn.exec(se());d&&(u+=d[0],l.index+=d[0].length),v({type:"text",value:u});continue}if(c&&(c.type==="globstar"||c.star===!0)){c.type="star",c.star=!0,c.value+=u,c.output=M,l.backtrack=!0,l.globstar=!0,z(u);continue}let m=se();if(r.noextglob!==!0&&/^\([^?]/.test(m)){Se("star",u);continue}if(c.type==="star"){if(r.noglobstar===!0){z(u);continue}let d=c.prev,S=d.prev,P=d.type==="slash"||d.type==="bos",F=S&&(S.type==="star"||S.type==="globstar");if(r.bash===!0&&(!P||m[0]&&m[0]!=="/")){v({type:"star",value:u,output:""});continue}let q=l.braces>0&&(d.type==="comma"||d.type==="brace"),Me=H.length&&(d.type==="pipe"||d.type==="paren");if(!P&&d.type!=="paren"&&!q&&!Me){v({type:"star",value:u,output:""});continue}for(;m.slice(0,3)==="/**";){let ve=e[l.index+4];if(ve&&ve!=="/")break;m=m.slice(3),z("/**",3)}if(d.type==="bos"&&I()){c.type="globstar",c.value+=u,c.output=E(r),l.output=c.output,l.globstar=!0,z(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&!F&&I()){l.output=l.output.slice(0,-(d.output+c.output).length),d.output=`(?:${d.output}`,c.type="globstar",c.output=E(r)+(r.strictSlashes?")":"|$)"),c.value+=u,l.globstar=!0,l.output+=d.output+c.output,z(u);continue}if(d.type==="slash"&&d.prev.type!=="bos"&&m[0]==="/"){let ve=m[1]!==void 0?"|$":"";l.output=l.output.slice(0,-(d.output+c.output).length),d.output=`(?:${d.output}`,c.type="globstar",c.output=`${E(r)}${k}|${k}${ve})`,c.value+=u,l.output+=d.output+c.output,l.globstar=!0,z(u+ee()),v({type:"slash",value:"/",output:""});continue}if(d.type==="bos"&&m[0]==="/"){c.type="globstar",c.value+=u,c.output=`(?:^|${k}|${E(r)}${k})`,l.output=c.output,l.globstar=!0,z(u+ee()),v({type:"slash",value:"/",output:""});continue}l.output=l.output.slice(0,-c.output.length),c.type="globstar",c.output=E(r),c.value+=u,l.output+=c.output,l.globstar=!0,z(u);continue}let L={type:"star",value:u,output:M};if(r.bash===!0){L.output=".*?",(c.type==="bos"||c.type==="slash")&&(L.output=b+L.output),v(L);continue}if(c&&(c.type==="bracket"||c.type==="paren")&&r.regex===!0){L.output=u,v(L);continue}(l.index===l.start||c.type==="slash"||c.type==="dot")&&(c.type==="dot"?(l.output+=x,c.output+=x):r.dot===!0?(l.output+=T,c.output+=T):(l.output+=b,c.output+=b),$()!=="*"&&(l.output+=y,c.output+=y)),v(L)}for(;l.brackets>0;){if(r.strictBrackets===!0)throw new SyntaxError(de("closing","]"));l.output=J.escapeLast(l.output,"["),ue("brackets")}for(;l.parens>0;){if(r.strictBrackets===!0)throw new SyntaxError(de("closing",")"));l.output=J.escapeLast(l.output,"("),ue("parens")}for(;l.braces>0;){if(r.strictBrackets===!0)throw new SyntaxError(de("closing","}"));l.output=J.escapeLast(l.output,"{"),ue("braces")}if(r.strictSlashes!==!0&&(c.type==="star"||c.type==="bracket")&&v({type:"maybe_slash",value:"",output:`${k}?`}),l.backtrack===!0){l.output="";for(let m of l.tokens)l.output+=m.output!=null?m.output:m.value,m.suffix&&(l.output+=m.suffix)}return l};nr.fastpaths=(e,t)=>{let r=B({},t),n=typeof r.maxLength=="number"?Math.min(Ne,r.maxLength):Ne,s=e.length;if(s>n)throw new SyntaxError(`Input length: ${s}, exceeds maximum allowed length: ${n}`);e=rr[e]||e;let a=J.isWindows(t),{DOT_LITERAL:i,SLASH_LITERAL:o,ONE_CHAR:h,DOTS_SLASH:g,NO_DOT:f,NO_DOTS:A,NO_DOTS_SLASH:p,STAR:k,START_ANCHOR:y}=Oe.globChars(a),R=r.dot?A:f,_=r.dot?p:f,x=r.capture?"":"?:",T={negated:!1,prefix:""},O=r.bash===!0?".*?":k;r.capture&&(O=`(${O})`);let W=b=>b.noglobstar===!0?O:`(${x}(?:(?!${y}${b.dot?g:i}).)*?)`,G=b=>{switch(b){case"*":return`${R}${h}${O}`;case".*":return`${i}${h}${O}`;case"*.*":return`${R}${O}${i}${h}${O}`;case"*/*":return`${R}${O}${o}${h}${_}${O}`;case"**":return R+W(r);case"**/*":return`(?:${R}${W(r)}${o})?${_}${h}${O}`;case"**/*.*":return`(?:${R}${W(r)}${o})?${_}${O}${i}${h}${O}`;case"**/.*":return`(?:${R}${W(r)}${o})?${i}${h}${O}`;default:{let C=/^(.*?)\.(\w+)$/.exec(b);if(!C)return;let M=G(C[1]);return M?M+i+C[2]:void 0}}},ne=J.removePrefix(e,T),E=G(ne);return E&&r.strictSlashes!==!0&&(E+=`${o}?`),E};tr.exports=nr});var ir=K((ys,ar)=>{"use strict";var Fn=require("path"),Qn=er(),Ye=sr(),ze=be(),Xn=ye(),Zn=e=>e&&typeof e=="object"&&!Array.isArray(e),D=(e,t,r=!1)=>{if(Array.isArray(e)){let f=e.map(p=>D(p,t,r));return p=>{for(let k of f){let y=k(p);if(y)return y}return!1}}let n=Zn(e)&&e.tokens&&e.input;if(e===""||typeof e!="string"&&!n)throw new TypeError("Expected pattern to be a non-empty string");let s=t||{},a=ze.isWindows(t),i=n?D.compileRe(e,t):D.makeRe(e,t,!1,!0),o=i.state;delete i.state;let h=()=>!1;if(s.ignore){let f=Q(B({},t),{ignore:null,onMatch:null,onResult:null});h=D(s.ignore,f,r)}let g=(f,A=!1)=>{let{isMatch:p,match:k,output:y}=D.test(f,i,t,{glob:e,posix:a}),R={glob:e,state:o,regex:i,posix:a,input:f,output:y,match:k,isMatch:p};return typeof s.onResult=="function"&&s.onResult(R),p===!1?(R.isMatch=!1,A?R:!1):h(f)?(typeof s.onIgnore=="function"&&s.onIgnore(R),R.isMatch=!1,A?R:!1):(typeof s.onMatch=="function"&&s.onMatch(R),A?R:!0)};return r&&(g.state=o),g};D.test=(e,t,r,{glob:n,posix:s}={})=>{if(typeof e!="string")throw new TypeError("Expected input to be a string");if(e==="")return{isMatch:!1,output:""};let a=r||{},i=a.format||(s?ze.toPosixSlashes:null),o=e===n,h=o&&i?i(e):e;return o===!1&&(h=i?i(e):e,o=h===n),(o===!1||a.capture===!0)&&(a.matchBase===!0||a.basename===!0?o=D.matchBase(e,t,r,s):o=t.exec(h)),{isMatch:Boolean(o),match:o,output:h}};D.matchBase=(e,t,r,n=ze.isWindows(r))=>(t instanceof RegExp?t:D.makeRe(t,r)).test(Fn.basename(e));D.isMatch=(e,t,r)=>D(t,r)(e);D.parse=(e,t)=>Array.isArray(e)?e.map(r=>D.parse(r,t)):Ye(e,Q(B({},t),{fastpaths:!1}));D.scan=(e,t)=>Qn(e,t);D.compileRe=(e,t,r=!1,n=!1)=>{if(r===!0)return e.output;let s=t||{},a=s.contains?"":"^",i=s.contains?"":"$",o=`${a}(?:${e.output})${i}`;e&&e.negated===!0&&(o=`^(?!${o}).*$`);let h=D.toRegex(o,t);return n===!0&&(h.state=e),h};D.makeRe=(e,t={},r=!1,n=!1)=>{if(!e||typeof e!="string")throw new TypeError("Expected a non-empty string");let s={negated:!1,fastpaths:!0};return t.fastpaths!==!1&&(e[0]==="."||e[0]==="*")&&(s.output=Ye.fastpaths(e,t)),s.output||(s=Ye(e,t)),D.compileRe(s,t,r,n)};D.toRegex=(e,t)=>{try{let r=t||{};return new RegExp(e,r.flags||(r.nocase?"i":""))}catch(r){if(t&&t.debug===!0)throw r;return/$^/}};D.constants=Xn;ar.exports=D});var cr=K((bs,or)=>{"use strict";or.exports=ir()});var hr=K((_s,ur)=>{"use strict";var lr=require("util"),pr=Gt(),oe=cr(),Ve=be(),fr=e=>e===""||e==="./",N=(e,t,r)=>{t=[].concat(t),e=[].concat(e);let n=new Set,s=new Set,a=new Set,i=0,o=f=>{a.add(f.output),r&&r.onResult&&r.onResult(f)};for(let f=0;f!n.has(f));if(r&&g.length===0){if(r.failglob===!0)throw new Error(`No matches found for "${t.join(", ")}"`);if(r.nonull===!0||r.nullglob===!0)return r.unescape?t.map(f=>f.replace(/\\/g,"")):t}return g};N.match=N;N.matcher=(e,t)=>oe(e,t);N.isMatch=(e,t,r)=>oe(t,r)(e);N.any=N.isMatch;N.not=(e,t,r={})=>{t=[].concat(t).map(String);let n=new Set,s=[],a=o=>{r.onResult&&r.onResult(o),s.push(o.output)},i=N(e,t,Q(B({},r),{onResult:a}));for(let o of s)i.includes(o)||n.add(o);return[...n]};N.contains=(e,t,r)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${lr.inspect(e)}"`);if(Array.isArray(t))return t.some(n=>N.contains(e,n,r));if(typeof t=="string"){if(fr(e)||fr(t))return!1;if(e.includes(t)||e.startsWith("./")&&e.slice(2).includes(t))return!0}return N.isMatch(e,t,Q(B({},r),{contains:!0}))};N.matchKeys=(e,t,r)=>{if(!Ve.isObject(e))throw new TypeError("Expected the first argument to be an object");let n=N(Object.keys(e),t,r),s={};for(let a of n)s[a]=e[a];return s};N.some=(e,t,r)=>{let n=[].concat(e);for(let s of[].concat(t)){let a=oe(String(s),r);if(n.some(i=>a(i)))return!0}return!1};N.every=(e,t,r)=>{let n=[].concat(e);for(let s of[].concat(t)){let a=oe(String(s),r);if(!n.every(i=>a(i)))return!1}return!0};N.all=(e,t,r)=>{if(typeof e!="string")throw new TypeError(`Expected a string: "${lr.inspect(e)}"`);return[].concat(t).every(n=>oe(n,r)(e))};N.capture=(e,t,r)=>{let n=Ve.isWindows(r),a=oe.makeRe(String(e),Q(B({},r),{capture:!0})).exec(n?Ve.toPosixSlashes(t):t);if(a)return a.slice(1).map(i=>i===void 0?"":i)};N.makeRe=(...e)=>oe.makeRe(...e);N.scan=(...e)=>oe.scan(...e);N.parse=(e,t)=>{let r=[];for(let n of[].concat(e||[]))for(let s of pr(String(n),t))r.push(oe.parse(s,t));return r};N.braces=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");return t&&t.nobrace===!0||!/\{.*\}/.test(e)?[e]:pr(e,t)};N.braceExpand=(e,t)=>{if(typeof e!="string")throw new TypeError("Expected a string");return N.braces(e,Q(B({},t),{expand:!0}))};ur.exports=N});var gr=K((Es,dr)=>{"use strict";dr.exports=(e,...t)=>new Promise(r=>{r(e(...t))})});var Ar=K((xs,Je)=>{"use strict";var Yn=gr(),mr=e=>{if(e<1)throw new TypeError("Expected `concurrency` to be a number from 1 and up");let t=[],r=0,n=()=>{r--,t.length>0&&t.shift()()},s=(o,h,...g)=>{r++;let f=Yn(o,...g);h(f),f.then(n,n)},a=(o,h,...g)=>{rnew Promise(g=>a(o,g,...h));return Object.defineProperties(i,{activeCount:{get:()=>r},pendingCount:{get:()=>t.length}}),i};Je.exports=mr;Je.exports.default=mr});var Vn={};Or(Vn,{default:()=>es});var He=X(require("@yarnpkg/cli")),ae=X(require("@yarnpkg/core")),nt=X(require("@yarnpkg/core")),le=X(require("clipanion")),Ae=class extends He.BaseCommand{constructor(){super(...arguments);this.json=le.Option.Boolean("--json",!1,{description:"Format the output as an NDJSON stream"});this.production=le.Option.Boolean("--production",!1,{description:"Only install regular dependencies by omitting dev dependencies"});this.all=le.Option.Boolean("-A,--all",!1,{description:"Install the entire project"});this.workspaces=le.Option.Rest()}async execute(){let t=await ae.Configuration.find(this.context.cwd,this.context.plugins),{project:r,workspace:n}=await ae.Project.find(t,this.context.cwd),s=await ae.Cache.find(t);await r.restoreInstallState({restoreResolutions:!1});let a;if(this.all)a=new Set(r.workspaces);else if(this.workspaces.length===0){if(!n)throw new He.WorkspaceRequiredError(r.cwd,this.context.cwd);a=new Set([n])}else a=new Set(this.workspaces.map(o=>r.getWorkspaceByIdent(nt.structUtils.parseIdent(o))));for(let o of a)for(let h of this.production?["dependencies"]:ae.Manifest.hardDependencies)for(let g of o.manifest.getForScope(h).values()){let f=r.tryWorkspaceByDescriptor(g);f!==null&&a.add(f)}for(let o of r.workspaces)a.has(o)?this.production&&o.manifest.devDependencies.clear():(o.manifest.installConfig=o.manifest.installConfig||{},o.manifest.installConfig.selfReferences=!1,o.manifest.dependencies.clear(),o.manifest.devDependencies.clear(),o.manifest.peerDependencies.clear(),o.manifest.scripts.clear());return(await ae.StreamReport.start({configuration:t,json:this.json,stdout:this.context.stdout,includeLogs:!0},async o=>{await r.install({cache:s,report:o,persistProject:!1})})).exitCode()}};Ae.paths=[["workspaces","focus"]],Ae.usage=le.Command.Usage({category:"Workspace-related commands",description:"install a single workspace and its dependencies",details:"\n This command will run an install as if the specified workspaces (and all other workspaces they depend on) were the only ones in the project. If no workspaces are explicitly listed, the active one will be assumed.\n\n Note that this command is only very moderately useful when using zero-installs, since the cache will contain all the packages anyway - meaning that the only difference between a full install and a focused install would just be a few extra lines in the `.pnp.cjs` file, at the cost of introducing an extra complexity.\n\n If the `-A,--all` flag is set, the entire project will be installed. Combine with `--production` to replicate the old `yarn install --production`.\n "});var st=Ae;var Ie=X(require("@yarnpkg/cli")),ge=X(require("@yarnpkg/core")),Ee=X(require("@yarnpkg/core")),Y=X(require("@yarnpkg/core")),Rr=X(require("@yarnpkg/plugin-git")),U=X(require("clipanion")),Be=X(hr()),yr=X(require("os")),br=X(Ar()),re=X(require("typanion")),xe=class extends Ie.BaseCommand{constructor(){super(...arguments);this.recursive=U.Option.Boolean("-R,--recursive",!1,{description:"Find packages via dependencies/devDependencies instead of using the workspaces field"});this.from=U.Option.Array("--from",[],{description:"An array of glob pattern idents from which to base any recursion"});this.all=U.Option.Boolean("-A,--all",!1,{description:"Run the command on all workspaces of a project"});this.verbose=U.Option.Boolean("-v,--verbose",!1,{description:"Prefix each output line with the name of the originating workspace"});this.parallel=U.Option.Boolean("-p,--parallel",!1,{description:"Run the commands in parallel"});this.interlaced=U.Option.Boolean("-i,--interlaced",!1,{description:"Print the output of commands in real-time instead of buffering it"});this.jobs=U.Option.String("-j,--jobs",{description:"The maximum number of parallel tasks that the execution will be limited to; or `unlimited`",validator:re.isOneOf([re.isEnum(["unlimited"]),re.applyCascade(re.isNumber(),[re.isInteger(),re.isAtLeast(1)])])});this.topological=U.Option.Boolean("-t,--topological",!1,{description:"Run the command after all workspaces it depends on (regular) have finished"});this.topologicalDev=U.Option.Boolean("--topological-dev",!1,{description:"Run the command after all workspaces it depends on (regular + dev) have finished"});this.include=U.Option.Array("--include",[],{description:"An array of glob pattern idents; only matching workspaces will be traversed"});this.exclude=U.Option.Array("--exclude",[],{description:"An array of glob pattern idents; matching workspaces won't be traversed"});this.publicOnly=U.Option.Boolean("--no-private",{description:"Avoid running the command on private workspaces"});this.since=U.Option.String("--since",{description:"Only include workspaces that have been changed since the specified ref.",tolerateBoolean:!0});this.commandName=U.Option.String();this.args=U.Option.Proxy()}async execute(){let t=await ge.Configuration.find(this.context.cwd,this.context.plugins),{project:r,workspace:n}=await ge.Project.find(t,this.context.cwd);if(!this.all&&!n)throw new Ie.WorkspaceRequiredError(r.cwd,this.context.cwd);await r.restoreInstallState();let s=this.cli.process([this.commandName,...this.args]),a=s.path.length===1&&s.path[0]==="run"&&typeof s.scriptName!="undefined"?s.scriptName:null;if(s.path.length===0)throw new U.UsageError("Invalid subcommand name for iteration - use the 'run' keyword if you wish to execute a script");let i=this.all?r.topLevelWorkspace:n,o=this.since?Array.from(await Rr.gitUtils.fetchChangedWorkspaces({ref:this.since,project:r})):[i,...this.from.length>0?i.getRecursiveWorkspaceChildren():[]],h=E=>Be.default.isMatch(Y.structUtils.stringifyIdent(E.locator),this.from),g=this.from.length>0?o.filter(h):o,f=new Set([...g,...g.map(E=>[...this.recursive?this.since?E.getRecursiveWorkspaceDependents():E.getRecursiveWorkspaceDependencies():E.getRecursiveWorkspaceChildren()]).flat()]),A=[],p=!1;if(a==null?void 0:a.includes(":")){for(let E of r.workspaces)if(E.manifest.scripts.has(a)&&(p=!p,p===!1))break}for(let E of f)a&&!E.manifest.scripts.has(a)&&!p&&!(await ge.scriptUtils.getWorkspaceAccessibleBinaries(E)).has(a)||a===process.env.npm_lifecycle_event&&E.cwd===n.cwd||this.include.length>0&&!Be.default.isMatch(Y.structUtils.stringifyIdent(E.locator),this.include)||this.exclude.length>0&&Be.default.isMatch(Y.structUtils.stringifyIdent(E.locator),this.exclude)||this.publicOnly&&E.manifest.private===!0||A.push(E);let k=this.parallel?this.jobs==="unlimited"?Infinity:this.jobs||Math.max(1,(0,yr.cpus)().length/2):1,y=k===1?!1:this.parallel,R=y?this.interlaced:!0,_=(0,br.default)(k),x=new Map,T=new Set,O=0,W=null,G=!1,ne=await Ee.StreamReport.start({configuration:t,stdout:this.context.stdout},async E=>{let b=async(C,{commandIndex:M})=>{if(G)return-1;!y&&this.verbose&&M>1&&E.reportSeparator();let l=zn(C,{configuration:t,verbose:this.verbose,commandIndex:M}),[H,w]=_r(E,{prefix:l,interlaced:R}),[j,c]=_r(E,{prefix:l,interlaced:R});try{this.verbose&&E.reportInfo(null,`${l} Process started`);let u=Date.now(),I=await this.cli.run([this.commandName,...this.args],{cwd:C.cwd,stdout:H,stderr:j})||0;H.end(),j.end(),await w,await c;let $=Date.now();if(this.verbose){let ee=t.get("enableTimers")?`, completed in ${Y.formatUtils.pretty(t,$-u,Y.formatUtils.Type.DURATION)}`:"";E.reportInfo(null,`${l} Process exited (exit code ${I})${ee}`)}return I===130&&(G=!0,W=I),I}catch(u){throw H.end(),j.end(),await w,await c,u}};for(let C of A)x.set(C.anchoredLocator.locatorHash,C);for(;x.size>0&&!E.hasErrors();){let C=[];for(let[H,w]of x){if(T.has(w.anchoredDescriptor.descriptorHash))continue;let j=!0;if(this.topological||this.topologicalDev){let c=this.topologicalDev?new Map([...w.manifest.dependencies,...w.manifest.devDependencies]):w.manifest.dependencies;for(let u of c.values()){let I=r.tryWorkspaceByDescriptor(u);if(j=I===null||!x.has(I.anchoredLocator.locatorHash),!j)break}}if(!!j&&(T.add(w.anchoredDescriptor.descriptorHash),C.push(_(async()=>{let c=await b(w,{commandIndex:++O});return x.delete(H),T.delete(w.anchoredDescriptor.descriptorHash),c})),!y))break}if(C.length===0){let H=Array.from(x.values()).map(w=>Y.structUtils.prettyLocator(t,w.anchoredLocator)).join(", ");E.reportError(Ee.MessageName.CYCLIC_DEPENDENCIES,`Dependency cycle detected (${H})`);return}let l=(await Promise.all(C)).find(H=>H!==0);W===null&&(W=typeof l!="undefined"?1:W),(this.topological||this.topologicalDev)&&typeof l!="undefined"&&E.reportError(Ee.MessageName.UNNAMED,"The command failed for workspaces that are depended upon by other workspaces; can't satisfy the dependency graph")}});return W!==null?W:ne.exitCode()}};xe.paths=[["workspaces","foreach"]],xe.usage=U.Command.Usage({category:"Workspace-related commands",description:"run a command on all workspaces",details:"\n This command will run a given sub-command on current and all its descendant workspaces. Various flags can alter the exact behavior of the command:\n\n - If `-p,--parallel` is set, the commands will be ran in parallel; they'll by default be limited to a number of parallel tasks roughly equal to half your core number, but that can be overridden via `-j,--jobs`, or disabled by setting `-j unlimited`.\n\n - If `-p,--parallel` and `-i,--interlaced` are both set, Yarn will print the lines from the output as it receives them. If `-i,--interlaced` wasn't set, it would instead buffer the output from each process and print the resulting buffers only after their source processes have exited.\n\n - If `-t,--topological` is set, Yarn will only run the command after all workspaces that it depends on through the `dependencies` field have successfully finished executing. If `--topological-dev` is set, both the `dependencies` and `devDependencies` fields will be considered when figuring out the wait points.\n\n - If `-A,--all` is set, Yarn will run the command on all the workspaces of a project. By default yarn runs the command only on current and all its descendant workspaces.\n\n - If `-R,--recursive` is set, Yarn will find workspaces to run the command on by recursively evaluating `dependencies` and `devDependencies` fields, instead of looking at the `workspaces` fields.\n\n - If `--from` is set, Yarn will use the packages matching the 'from' glob as the starting point for any recursive search.\n\n - If `--since` is set, Yarn will only run the command on workspaces that have been modified since the specified ref. By default Yarn will use the refs specified by the `changesetBaseRefs` configuration option.\n\n - The command may apply to only some workspaces through the use of `--include` which acts as a whitelist. The `--exclude` flag will do the opposite and will be a list of packages that mustn't execute the script. Both flags accept glob patterns (if valid Idents and supported by [micromatch](https://github.com/micromatch/micromatch)). Make sure to escape the patterns, to prevent your own shell from trying to expand them.\n\n Adding the `-v,--verbose` flag will cause Yarn to print more information; in particular the name of the workspace that generated the output will be printed at the front of each line.\n\n If the command is `run` and the script being run does not exist the child workspace will be skipped without error.\n ",examples:[["Publish current and all descendant packages","yarn workspaces foreach npm publish --tolerate-republish"],["Run build script on current and all descendant packages","yarn workspaces foreach run build"],["Run build script on current and all descendant packages in parallel, building package dependencies first","yarn workspaces foreach -pt run build"],["Run build script on several packages and all their dependencies, building dependencies first","yarn workspaces foreach -ptR --from '{workspace-a,workspace-b}' run build"]]});var Er=xe;function _r(e,{prefix:t,interlaced:r}){let n=e.createStreamReporter(t),s=new Y.miscUtils.DefaultStream;s.pipe(n,{end:!1}),s.on("finish",()=>{n.end()});let a=new Promise(o=>{n.on("finish",()=>{o(s.active)})});if(r)return[s,a];let i=new Y.miscUtils.BufferStream;return i.pipe(s,{end:!1}),i.on("finish",()=>{s.end()}),[i,a]}function zn(e,{configuration:t,commandIndex:r,verbose:n}){if(!n)return null;let s=Y.structUtils.convertToIdent(e.locator),i=`[${Y.structUtils.stringifyIdent(s)}]:`,o=["#2E86AB","#A23B72","#F18F01","#C73E1D","#CCE2A3"],h=o[r%o.length];return Y.formatUtils.pretty(t,i,h)}var Jn={commands:[st,Er]},es=Jn;return Vn;})(); 8 | /*! 9 | * fill-range 10 | * 11 | * Copyright (c) 2014-present, Jon Schlinkert. 12 | * Licensed under the MIT License. 13 | */ 14 | /*! 15 | * is-number 16 | * 17 | * Copyright (c) 2014-present, Jon Schlinkert. 18 | * Released under the MIT License. 19 | */ 20 | /*! 21 | * to-regex-range 22 | * 23 | * Copyright (c) 2015-present, Jon Schlinkert. 24 | * Released under the MIT License. 25 | */ 26 | return plugin; 27 | } 28 | }; 29 | --------------------------------------------------------------------------------