├── .env.example ├── .eslintrc.js ├── .github ├── CODEOWNERS ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── FUNDING.yml └── workflows │ ├── benchmark.yml │ ├── checks.yml │ ├── cli-tests.yml │ ├── codeql.yml │ └── publish.yml ├── .gitignore ├── .prettierrc.json ├── .yarn ├── plugins │ └── @yarnpkg │ │ ├── plugin-interactive-tools.cjs │ │ └── plugin-typescript.cjs └── releases │ └── yarn-3.3.0.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── apps ├── docs │ ├── babel.config.js │ ├── docs │ │ ├── .gitignore │ │ ├── Documentation │ │ │ └── _category_.json │ │ ├── getting-started │ │ │ ├── cli-framework │ │ │ │ ├── _category_.json │ │ │ │ ├── learning-the-workflow │ │ │ │ │ ├── _category_.json │ │ │ │ │ ├── creating-commands.md │ │ │ │ │ ├── deploying.md │ │ │ │ │ ├── dev-mode.md │ │ │ │ │ └── syncing.md │ │ │ │ └── prep-work │ │ │ │ │ ├── _category_.json │ │ │ │ │ ├── creating-a-new-project.md │ │ │ │ │ └── installing-node-and-disploy.md │ │ │ ├── framework-less │ │ │ │ ├── creating-commands.md │ │ │ │ ├── creating-main-file.mdx │ │ │ │ ├── deploying-commands.md │ │ │ │ ├── index.md │ │ │ │ ├── prep-work.mdx │ │ │ │ └── starting.mdx │ │ │ └── index.md │ │ └── introduction.md │ ├── docusaurus.config.js │ ├── package.json │ ├── sidebars.js │ ├── src │ │ ├── css │ │ │ └── custom.css │ │ └── pages │ │ │ ├── index.module.css │ │ │ ├── index.tsx │ │ │ └── markdown-page.md │ ├── static │ │ ├── .nojekyll │ │ └── img │ │ │ └── logo.svg │ ├── tailwind.config.js │ └── tsconfig.json ├── example │ ├── README.md │ ├── package.json │ ├── src │ │ ├── commands │ │ │ ├── ping.ts │ │ │ └── voice-test.ts │ │ ├── events │ │ │ └── message-ping.ts │ │ ├── index.ts │ │ └── types │ │ │ └── EventHandler.ts │ ├── tsconfig.json │ └── tsup.config.js └── framework-example │ ├── README.md │ ├── disploy.json │ ├── package.json │ ├── src │ ├── commands │ │ ├── channel.ts │ │ ├── classtest.ts │ │ ├── env.ts │ │ ├── hey.ts │ │ ├── image-test.ts │ │ ├── me.ts │ │ ├── member.ts │ │ └── ping.ts │ ├── handlers │ │ ├── ping.ts │ │ └── pingNoParams.ts │ ├── index.ts │ └── lib │ │ └── test.ts │ └── tsconfig.json ├── package.json ├── packages ├── disploy │ ├── .cliff-jumperrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── cli │ │ ├── assets │ │ │ └── code │ │ │ │ ├── array.js │ │ │ │ ├── cfWorkerEntry.js │ │ │ │ ├── devServerEntry.js │ │ │ │ └── standaloneEntry.js │ │ ├── src │ │ │ ├── Constants.ts │ │ │ ├── commands │ │ │ │ ├── build.ts │ │ │ │ ├── common │ │ │ │ │ └── build.ts │ │ │ │ ├── deploy.ts │ │ │ │ ├── dev.ts │ │ │ │ ├── sync.ts │ │ │ │ └── test-server.ts │ │ │ ├── disploy.ts │ │ │ ├── lib │ │ │ │ ├── EnvTools.ts │ │ │ │ ├── ProjectTools.ts │ │ │ │ ├── StringFormatters.ts │ │ │ │ ├── UserError.ts │ │ │ │ ├── WranglerWrapper.ts │ │ │ │ ├── compiler │ │ │ │ │ ├── assets │ │ │ │ │ │ └── index.ts │ │ │ │ │ ├── constants.ts │ │ │ │ │ ├── copyDir.ts │ │ │ │ │ ├── globExportBundle.ts │ │ │ │ │ └── index.ts │ │ │ │ ├── devServer │ │ │ │ │ └── index.ts │ │ │ │ ├── disployConf │ │ │ │ │ └── index.ts │ │ │ │ └── shell.ts │ │ │ ├── types.ts │ │ │ └── utils │ │ │ │ └── logger.ts │ │ └── tsconfig.json │ ├── cliff.toml │ ├── package.json │ ├── scripts │ │ ├── architect.mjs │ │ └── copyAssets.mjs │ ├── src │ │ ├── adapters │ │ │ ├── IAdapter.ts │ │ │ ├── expressAdapter.ts │ │ │ ├── index.ts │ │ │ └── nextAdapter.ts │ │ ├── client │ │ │ ├── App.ts │ │ │ └── index.ts │ │ ├── commands │ │ │ ├── Command.ts │ │ │ ├── CommandManager.ts │ │ │ └── index.ts │ │ ├── http │ │ │ ├── RequestorError.ts │ │ │ ├── TParams.ts │ │ │ ├── TRequest.ts │ │ │ ├── TResponse.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ ├── message-components │ │ │ ├── MessageComponentHandler.ts │ │ │ ├── MessageComponentManager.ts │ │ │ └── index.ts │ │ ├── router │ │ │ ├── ApplicationCommandRoute.ts │ │ │ ├── BaseRoute.ts │ │ │ ├── MessageComponentRoute.ts │ │ │ ├── RouteParams.test.ts │ │ │ ├── RouteParams.ts │ │ │ ├── Router.ts │ │ │ ├── RouterEvents.ts │ │ │ └── index.ts │ │ ├── structs │ │ │ ├── Attachment.ts │ │ │ ├── Base.ts │ │ │ ├── BaseChannel.ts │ │ │ ├── BaseInteraction.ts │ │ │ ├── ButtonInteraction.ts │ │ │ ├── ChannelMethods.ts │ │ │ ├── ChatInputInteraction.ts │ │ │ ├── ChatInputInteractionOptions.ts │ │ │ ├── ChatInputInteractionResolvedOptions.ts │ │ │ ├── CommandInteraction.ts │ │ │ ├── ContextMenuCommand.ts │ │ │ ├── Guild.ts │ │ │ ├── GuildBan.ts │ │ │ ├── GuildMember.ts │ │ │ ├── GuildTextChannel.ts │ │ │ ├── GuildVoiceChannel.ts │ │ │ ├── Message.ts │ │ │ ├── MessageComponentInteraction.ts │ │ │ ├── MessageContextMenuInteraction.ts │ │ │ ├── PartialChannel.ts │ │ │ ├── PartialGuildMember.ts │ │ │ ├── ToBeFetched.ts │ │ │ ├── User.ts │ │ │ ├── UserContextMenuInteraction.ts │ │ │ ├── UserInteraction.ts │ │ │ ├── index.ts │ │ │ └── managers │ │ │ │ ├── ChannelManager.ts │ │ │ │ ├── MessageManager.ts │ │ │ │ ├── StructureManager.test.ts │ │ │ │ ├── StructureManager.ts │ │ │ │ └── index.ts │ │ ├── types │ │ │ ├── DiscordChannel.ts │ │ │ ├── NonRuntimeClass.ts │ │ │ ├── StructureConstructor.ts │ │ │ └── index.ts │ │ └── utils │ │ │ ├── DiscordAPIUtils.ts │ │ │ ├── Logger.ts │ │ │ ├── RequestorError.ts │ │ │ ├── SnowflakeUtil.ts │ │ │ ├── Verify.ts │ │ │ ├── VerifyCFW.ts │ │ │ ├── VerifyNode.ts │ │ │ ├── index.ts │ │ │ └── runtime.ts │ ├── tsconfig.json │ └── tsup.config.js ├── eslint-config-custom │ ├── index.js │ └── package.json ├── rest │ ├── .cliff-jumperrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── cliff.toml │ ├── package.json │ ├── src │ │ ├── Constants.ts │ │ ├── Rest.ts │ │ ├── index.ts │ │ └── types │ │ │ ├── RestEvents.ts │ │ │ └── index.ts │ ├── tsconfig.json │ └── tsup.config.js ├── tsconfig │ ├── README.md │ ├── base.json │ ├── frontend-base.json │ ├── nextjs.json │ ├── package.json │ └── react-library.json └── ws │ ├── .cliff-jumperrc.json │ ├── CHANGELOG.md │ ├── README.md │ ├── cliff.toml │ ├── package.json │ ├── src │ ├── Gateway.ts │ ├── events │ │ ├── ChannelCreate.ts │ │ ├── ChannelDelete.ts │ │ ├── GuildCreate.ts │ │ ├── GuildDelete.ts │ │ ├── GuildMemberAdd.ts │ │ ├── GuildMemberRemove.ts │ │ ├── MessageCreate.ts │ │ └── _loader.ts │ ├── index.ts │ └── types.ts │ ├── tsconfig.json │ └── tsup.config.js ├── scripts ├── ayumi.mjs ├── benchmark.mjs └── changelog.mjs ├── tsup.config.js ├── turbo.json └── yarn.lock /.env.example: -------------------------------------------------------------------------------- 1 | DISCORD_TOKEN="" 2 | DISCORD_PUBLIC_KEY="" 3 | DISCORD_CLIENT_ID="" 4 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | // This tells ESLint to load the config from the package `eslint-config-custom` 4 | extends: ["custom"], 5 | ignores: ["**/assets/**"], 6 | settings: { 7 | next: { 8 | rootDir: ["apps/*/"], 9 | }, 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | packages/* @core-team 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Intro 2 | 3 | > Firstly, thank you so much for your consideration in contributing to Disploy. 💙 4 | 5 | Disploy uses Turborepo to manage our monorepo which we're using yarn to manage our dependencies with. 6 | You must have yarn installed to work on Disploy. 7 | 8 | Once installed run `yarn` at the root of the monorepo to install all our dependencies. 9 | 10 | # Development 11 | 12 | When working on Disploy it is recommended to use Node 18 and to run `yarn dev` in the root of the monorepo. 13 | This will start watching your changes and compile TypeScript on the fly. 14 | 15 | https://user-images.githubusercontent.com/69066026/197907414-7dcf6b84-e525-4471-b5d8-fc76597c9727.mp4 16 | 17 | When testing your changes, you should cd into `apps/example` and run `yarn serve` to start a local Disploy development server with hot reloading, make sure to still have `yarn dev` running in the root of the monorepo. 18 | 19 | ## Secrets 20 | 21 | We use `@meetuplol/envject` to inject environment variables from the root of the monorepo into our apps/examples. So make a `.env` in the root of the monorepo and not elsewhere. 22 | 23 | # Conversation 24 | 25 | We only use GitHub issues for bug reports and feature suggestions! If you have a question or want to chat about an issue or pull request, please keep it in a forum post on our Discord server. https://discord.gg/E3z8MDnTWn 26 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: [twisttaan] # Add Disploy when approved! 4 | patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | ko_fi: # Replace with a single Ko-fi username 7 | tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: # Replace with a single Liberapay username 10 | issuehunt: # Replace with a single IssueHunt username 11 | otechie: # Replace with a single Otechie username 12 | lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry 13 | custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] 14 | -------------------------------------------------------------------------------- /.github/workflows/benchmark.yml: -------------------------------------------------------------------------------- 1 | name: Tests 2 | 3 | on: 4 | pull_request: 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 7 | cancel-in-progress: true 8 | jobs: 9 | benchmark: 10 | name: Benchmark 11 | runs-on: ubuntu-latest 12 | env: 13 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 14 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 15 | 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v3 19 | 20 | - name: Install node.js v18 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 18 24 | cache: yarn 25 | 26 | - name: Install dependencies 27 | run: yarn --immutable 28 | 29 | - name: Build dependencies 30 | run: yarn build --filter=@disploy/example 31 | 32 | - name: Benchmark 33 | run: node scripts/benchmark.mjs 34 | env: 35 | DISCORD_TOKEN: ${{ secrets.DISCORD_TOKEN }} 36 | CLIENT_ID: ${{ secrets.CLIENT_ID }} 37 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 38 | GITHUB_REF: ${{ github.ref }} 39 | -------------------------------------------------------------------------------- /.github/workflows/checks.yml: -------------------------------------------------------------------------------- 1 | name: Checks 2 | 3 | on: 4 | push: 5 | pull_request: 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 8 | cancel-in-progress: true 9 | jobs: 10 | type-check: 11 | name: TypeScript Type Check 12 | runs-on: ubuntu-latest 13 | env: 14 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 15 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v3 19 | 20 | - name: Install node.js v18 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 18 24 | cache: yarn 25 | 26 | - name: Install dependencies 27 | run: yarn --immutable 28 | 29 | - name: Build dependencies 30 | run: yarn build 31 | 32 | - name: Type-check 33 | run: yarn type-check 34 | tests: 35 | name: Unit Tests 36 | runs-on: ubuntu-latest 37 | env: 38 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 39 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 40 | steps: 41 | - name: Checkout repository 42 | uses: actions/checkout@v3 43 | 44 | - name: Install node.js v18 45 | uses: actions/setup-node@v3 46 | with: 47 | node-version: 18 48 | cache: yarn 49 | 50 | - name: Install dependencies 51 | run: yarn --immutable 52 | 53 | - name: Build dependencies 54 | run: yarn build 55 | 56 | - name: Run unit tests 57 | run: yarn test 58 | -------------------------------------------------------------------------------- /.github/workflows/cli-tests.yml: -------------------------------------------------------------------------------- 1 | name: Disploy CLI Tests 2 | 3 | on: 4 | push: 5 | pull_request: 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} 8 | cancel-in-progress: true 9 | jobs: 10 | build_bot: 11 | name: Build example framework bot using the CLI on ${{ matrix.os }} 12 | runs-on: ${{ matrix.os }} 13 | strategy: 14 | matrix: 15 | os: [ubuntu-latest, windows-latest] 16 | 17 | env: 18 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 19 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 20 | steps: 21 | - name: Checkout repository 22 | uses: actions/checkout@v3 23 | 24 | - name: Install node.js v18 25 | uses: actions/setup-node@v3 26 | with: 27 | node-version: 18 28 | cache: yarn 29 | 30 | - name: Install dependencies 31 | run: yarn --immutable 32 | 33 | - name: Build dependencies 34 | run: yarn build --filter=@disploy/framework-example 35 | 36 | - name: Build the example bot 37 | run: yarn workspace @disploy/framework-example disploy build --skip-prebuild 38 | -------------------------------------------------------------------------------- /.github/workflows/codeql.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | # 7 | # ******** NOTE ******** 8 | # We have attempted to detect the languages in your repository. Please check 9 | # the `language` matrix defined below to confirm you have the correct set of 10 | # supported CodeQL languages. 11 | # 12 | name: "CodeQL" 13 | 14 | on: 15 | push: 16 | branches: [ "main" ] 17 | pull_request: 18 | # The branches below must be a subset of the branches above 19 | branches: [ "main" ] 20 | schedule: 21 | - cron: '42 17 * * 2' 22 | 23 | jobs: 24 | analyze: 25 | name: Analyze 26 | runs-on: ubuntu-latest 27 | permissions: 28 | actions: read 29 | contents: read 30 | security-events: write 31 | 32 | strategy: 33 | fail-fast: false 34 | matrix: 35 | language: [ 'typescript' ] 36 | # CodeQL supports [ 'cpp', 'csharp', 'go', 'java', 'javascript', 'python', 'ruby' ] 37 | # Learn more about CodeQL language support at https://aka.ms/codeql-docs/language-support 38 | 39 | steps: 40 | - name: Checkout repository 41 | uses: actions/checkout@v3 42 | 43 | # Initializes the CodeQL tools for scanning. 44 | - name: Initialize CodeQL 45 | uses: github/codeql-action/init@v2 46 | with: 47 | languages: ${{ matrix.language }} 48 | # If you wish to specify custom queries, you can do so here or in a config file. 49 | # By default, queries listed here will override any specified in a config file. 50 | # Prefix the list here with "+" to use these queries and those in the config file. 51 | 52 | # Details on CodeQL's query packs refer to : https://docs.github.com/en/code-security/code-scanning/automatically-scanning-your-code-for-vulnerabilities-and-errors/configuring-code-scanning#using-queries-in-ql-packs 53 | # queries: security-extended,security-and-quality 54 | 55 | 56 | # Autobuild attempts to build any compiled languages (C/C++, C#, Go, or Java). 57 | # If this step fails, then you should remove it and run the build manually (see below) 58 | - name: Autobuild 59 | uses: github/codeql-action/autobuild@v2 60 | 61 | # ℹ️ Command-line programs to run using the OS shell. 62 | # 📚 See https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#jobsjob_idstepsrun 63 | 64 | # If the Autobuild fails above, remove it and uncomment the following three lines. 65 | # modify them (or add more) to build your code if your project, please refer to the EXAMPLE below for guidance. 66 | 67 | # - run: | 68 | # echo "Run, Build Application using script" 69 | # ./location_of_script_within_repo/buildscript.sh 70 | 71 | - name: Perform CodeQL Analysis 72 | uses: github/codeql-action/analyze@v2 73 | with: 74 | category: "/language:${{matrix.language}}" 75 | -------------------------------------------------------------------------------- /.github/workflows/publish.yml: -------------------------------------------------------------------------------- 1 | name: Publish Development Build 2 | 3 | on: 4 | schedule: 5 | - cron: '0 */12 * * *' 6 | push: 7 | branches: 8 | - main 9 | paths: 10 | - '.github/workflows/publish.yml' 11 | workflow_dispatch: 12 | 13 | jobs: 14 | npm: 15 | name: npm 16 | strategy: 17 | fail-fast: false 18 | matrix: 19 | include: 20 | - package: 'disploy' 21 | folder: 'disploy' 22 | - package: '@disploy/ws' 23 | folder: 'ws' 24 | - package: '@disploy/rest' 25 | folder: 'rest' 26 | runs-on: ubuntu-latest 27 | env: 28 | TURBO_TOKEN: ${{ secrets.TURBO_TOKEN }} 29 | TURBO_TEAM: ${{ secrets.TURBO_TEAM }} 30 | if: github.repository_owner == 'Disploy' 31 | 32 | steps: 33 | - name: Checkout repository 34 | uses: actions/checkout@v3 35 | 36 | - name: Install node.js v18 37 | uses: actions/setup-node@v3 38 | with: 39 | node-version: 18 40 | registry-url: https://registry.npmjs.org/ 41 | cache: yarn 42 | 43 | - name: Install dependencies 44 | run: yarn --immutable 45 | 46 | - name: Build dependencies 47 | run: yarn build 48 | 49 | - name: Publish package 50 | run: | 51 | yarn workspace ${{ matrix.package }} release --preid "dev.$(date +%s)-$(git rev-parse --short HEAD)" 52 | yarn workspace ${{ matrix.package }} npm publish --tag dev || true 53 | env: 54 | YARN_NPM_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} 55 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # testing 9 | coverage 10 | 11 | # next.js 12 | .next/ 13 | out/ 14 | build 15 | 16 | # development 17 | dist/ 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | .vscode/ 23 | *.mp3 24 | 25 | # debug 26 | npm-debug.log* 27 | yarn-debug.log* 28 | yarn-error.log* 29 | .pnpm-debug.log* 30 | 31 | # local env files 32 | .env.local 33 | .env.development.local 34 | .env.test.local 35 | .env.production.local 36 | .env 37 | 38 | # turbo 39 | .turbo 40 | 41 | # docusaurus 42 | .docusaurus 43 | .cache-loader 44 | /build 45 | 46 | # disploy 47 | .disploy 48 | 49 | # yarn 50 | .pnp.* 51 | .yarn/* 52 | !.yarn/patches 53 | !.yarn/plugins 54 | !.yarn/releases 55 | !.yarn/sdks 56 | !.yarn/versions 57 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "endOfLine": "lf", 3 | "printWidth": 120, 4 | "quoteProps": "as-needed", 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "useTabs": true 8 | } 9 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | enableGlobalCache: true 2 | 3 | nodeLinker: node-modules 4 | 5 | plugins: 6 | - path: .yarn/plugins/@yarnpkg/plugin-typescript.cjs 7 | spec: "@yarnpkg/plugin-typescript" 8 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 9 | spec: "@yarnpkg/plugin-interactive-tools" 10 | 11 | yarnPath: .yarn/releases/yarn-3.3.0.cjs 12 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ./packages/disploy/README.md -------------------------------------------------------------------------------- /apps/docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /apps/docs/docs/.gitignore: -------------------------------------------------------------------------------- 1 | # typedoc 2 | Documentation/**/* 3 | !Documentation/_category_.json -------------------------------------------------------------------------------- /apps/docs/docs/Documentation/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Documentation", 3 | "position": 5 4 | } 5 | -------------------------------------------------------------------------------- /apps/docs/docs/getting-started/cli-framework/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "CLI Framework", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Learn how to use the CLI framework." 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/docs/docs/getting-started/cli-framework/learning-the-workflow/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Learning the workflow", 3 | "position": 3, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Learn how to use the Disploy workflow" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/docs/docs/getting-started/cli-framework/learning-the-workflow/creating-commands.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Creating commands 6 | 7 | Commands are easy to create with Disploy, simply export default classes which extend the [`Command` class](/docs/Documentation/disploy/classes/Command) and store them in the `commands` directory. 8 | 9 | ``` 10 | ├── commands 11 | │ └── ping.ts 12 | ``` 13 | 14 | ```ts 15 | // commands/ping.ts 16 | import type { Command } from 'disploy'; 17 | 18 | export default { 19 | name: 'ping', 20 | description: 'pong!', 21 | 22 | async run(interaction) { 23 | return void interaction.reply({ 24 | content: 'hello world!!!!!!!!', 25 | }); 26 | }, 27 | } satisfies Command; 28 | ``` 29 | 30 | ## So what's going on here? 31 | 32 | Firstly, we're importing the `Command` type from Disploy. This is the type used by all commands. You can learn more about interaction types in the [Discord documentation](https://discord.com/developers/docs/interactions/application-commands#application-command-object-application-command-types). We then export a default object which conforms to the `Command` type. This object is the command itself. We then define a constructor for the class. 33 | 34 | ### `run` 35 | 36 | The `run` method is called when the command is run. It takes a single argument, the interaction. The interaction is an object which contains information about the interaction, such as the user who ran the command, the arguments they provided, and the channel the command was run in. 37 | 38 | The `run` method must return `Promise`, once returned the command will be considered finished; in serverless environments this will kill the process. This allows you to run code after initially responding to the interaction. This is useful for doing things like sending follow-up messages or deferring the interaction to do a long-running task and editing the interaction later. 39 | 40 |
41 | Example of a command with follow-up messages 42 | 43 | ```ts 44 | import type { Command } from 'disploy'; 45 | 46 | export default { 47 | name: 'hey', 48 | description: 'heyy!', 49 | 50 | async run(interaction) { 51 | interaction.deferReply(); 52 | 53 | await new Promise((resolve) => setTimeout(resolve, 2000)); 54 | 55 | return void interaction.editReply({ 56 | content: `Just wanted to say hey!`, 57 | }); 58 | }, 59 | } satisfies Command; 60 | ``` 61 | 62 |
63 | -------------------------------------------------------------------------------- /apps/docs/docs/getting-started/cli-framework/learning-the-workflow/deploying.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Deploying 6 | 7 | Disploy allows you to deploy your Discord application to your target of choice expressed in the `disploy.json` file. We currently only support deploying to [Cloudflare Workers](https://workers.cloudflare.com/). 8 | 9 | ## Deploying to Cloudflare Workers 10 | 11 | To deploy to Cloudflare Workers, you need to have a Cloudflare account, a Cloudflare Workers subscription and [wrangler](https://developers.cloudflare.com/workers/wrangler/) installed and configured. 12 | 13 | - You can sign up for a Cloudflare account [here](https://dash.cloudflare.com/sign-up). 14 | - Sign up for a Cloudflare Workers subscription [here](https://dash.cloudflare.com/sign-up/workers) (you can sign up for a free Workers subscription with [limited usage](https://developers.cloudflare.com/workers/platform/pricing), 100,000 requests per day). 15 | - Install wrangler via npm by running the following command: 16 | 17 | ```bash 18 | npm install -g wrangler 19 | ``` 20 | 21 | - Configure wrangler by running the following command: 22 | 23 | ```bash 24 | wrangler login 25 | ``` 26 | 27 | Once you have a Cloudflare account and a Cloudflare Workers subscription, you can deploy your Discord application by running the following command: 28 | 29 | ```bash 30 | disploy deploy 31 | ``` 32 | 33 | This will deploy your Discord application to a Cloudflare Worker defined in the `disploy.json` file. 34 | 35 | ```json 36 | { 37 | "prebuild": "yarn run build", 38 | "root": "dist", 39 | "target": { 40 | "type": "cloudflare", 41 | "name": "cf-example" // The name of your Cloudflare Worker 42 | } 43 | } 44 | ``` 45 | 46 | ### Configuring environment variables 47 | 48 | You will need to configure the following environment variables in your Cloudflare Worker: 49 | 50 | - `CLIENT_ID`: The client ID of your Discord application. 51 | - `PUBLIC_KEY`: The public key of your Discord application. 52 | - `TOKEN`: The token of your Discord application. 53 | 54 | You can find the client ID, public key and token of your Discord application in the [Discord Developer Portal](https://discord.com/developers/applications). 55 | 56 |
57 | Click to see how to find the client ID, public key and token of your Discord application 58 | 59 | ![](https://cdn.discordapp.com/attachments/1038456602610651196/1038456753672695848/id-and-pubkey.png) 60 | 61 | ![](https://cdn.discordapp.com/attachments/1038456602610651196/1038456754436063332/token.png) 62 | 63 |
64 | 65 | You can configure environment variables in your Cloudflare Worker on the [Workers dashboard](https://dash.cloudflare.com/?to=/:account/workers) by clicking on the name of your Cloudflare Worker, clicking on the "Settings" tab and then clicking on the "Environment" tab. 66 | 67 | :::warning 68 | 69 | Make sure to "Encrypt" the environment variables before saving them, this will stop `disploy deploy` from overwriting them. 70 | 71 | ::: 72 | 73 | ![](https://cdn.discordapp.com/attachments/1038456602610651196/1038456753316184094/cf-vars.png) 74 | 75 | Finally you can set the HTTP interactions endpoint URL of your Discord application to the URL of your Cloudflare Worker. You can find the URL printed in the console when you run `disploy deploy` or by clicking on the name of your Cloudflare Worker on the [Workers dashboard](https://dash.cloudflare.com/?to=/:account/workers). 76 | 77 | Make sure to set the HTTP interactions endpoint URL to the URL of your Cloudflare Worker with the `/interactions` path appended to it. 78 | 79 | ![](https://cdn.discordapp.com/attachments/1038456602610651196/1038456754125688903/interactions-endpoint.png) 80 | -------------------------------------------------------------------------------- /apps/docs/docs/getting-started/cli-framework/learning-the-workflow/dev-mode.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Entering development mode 6 | 7 | Disploy allows you to enter development mode by running the following command: 8 | 9 | ```bash 10 | $ disploy dev 11 | 12 | Your bot is ready to receive interactions! 13 | 1. Visit https://discord.com/developers/applications/1033202906385625180/information 14 | 2. Set INTERACTIONS ENDPOINT URL to https://xxxx-xx-xxx-xxx-xx.ngrok.io/interactions 15 | ``` 16 | 17 | This will start a development server exposed to the web with a reverse-tunnel powered by ngrok that will automatically load changes when you make changes to your code. 18 | 19 | :::tip 20 | 21 | When using TypeScript, you will need to run `tsc --watch` in a separate terminal window to compile your code. Disploy will automatically restart when you make changes to your compiled code. 22 | 23 | ::: 24 | -------------------------------------------------------------------------------- /apps/docs/docs/getting-started/cli-framework/learning-the-workflow/syncing.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Syncing commands 6 | 7 | Disploy allows you to sync commands to your Discord application by running the following command: 8 | 9 | ```bash 10 | disploy sync 11 | ``` 12 | 13 | This will give you a choice to merge or overwrite the commands in your Discord application with the commands in your project. If you choose to merge, Disploy will only add new commands and update existing commands. If you choose to overwrite, Disploy will delete all commands in your Discord application and add the commands in your project. 14 | 15 | ## Syncing commands in development mode 16 | 17 | When you run `disploy dev`, Disploy will automatically sync commands to your Discord application. This means that you don't have to run `disploy sync` manually. 18 | -------------------------------------------------------------------------------- /apps/docs/docs/getting-started/cli-framework/prep-work/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Preparations", 3 | "position": 2, 4 | "link": { 5 | "type": "generated-index", 6 | "description": "Preparations" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/docs/docs/getting-started/cli-framework/prep-work/creating-a-new-project.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Creating a new project 6 | 7 | You can bootstrap a new project using `create-disploy-app`: 8 | 9 | ```bash 10 | npx create-disploy-app@latest 11 | # or 12 | yarn create disploy-app 13 | # or 14 | pnpm create disploy-app 15 | ``` 16 | 17 | > Make sure to select "Disploy CLI Framework (TypeScript)". 18 | 19 | ## Project structure 20 | 21 | ``` 22 | ├── commands # Commands 23 | │ └── ping.ts # Ping command 24 | ├── index.ts # Empty file to make TypeScript not compile each folder as a separate module 25 | ``` 26 | -------------------------------------------------------------------------------- /apps/docs/docs/getting-started/cli-framework/prep-work/installing-node-and-disploy.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Installing Node.js and the Disploy toolchain 6 | 7 | ## Installing Node.js 8 | 9 | Disploy requires Node.js version 18 or higher. You can download Node.js from [the official website](https://nodejs.org/en/), through [Volta](https://volta.sh/) or other alternatives. 10 | 11 | We recommend using Volta, as it allows you to easily switch between Node.js versions. You can install Volta by running the following command: 12 | 13 | ```bash 14 | # Install Volta (first time only) 15 | curl https://get.volta.sh | bash 16 | 17 | # Install Node.js v18 18 | volta install node@18 19 | 20 | # Install yarn 21 | volta install yarn 22 | ``` 23 | -------------------------------------------------------------------------------- /apps/docs/docs/getting-started/framework-less/creating-commands.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Creating commands 6 | 7 | In the previous page, we mentioned importing `commands` from `./commands/commands.mjs` and recursively loading them. In this page, we'll go over how to actually create that file. 8 | 9 | ## Creating the file 10 | 11 | First, we'll create the file. We'll call it `commands.mjs` and put it in a folder called `commands`. 12 | 13 | ```bash 14 | mkdir commands 15 | touch commands/commands.mjs 16 | ``` 17 | 18 | ```js 19 | // commands/commands.mjs 20 | import Ping from './core/ping.mjs'; 21 | 22 | export default [Ping]; 23 | ``` 24 | 25 | This file is the entry point for all of our commands. It's where we'll import all of our commands and export them as an array. 26 | 27 | ## Creating the command 28 | 29 | Now, we'll create the command. We'll call it `ping` and put it in a folder called `core`. 30 | 31 | ```bash 32 | mkdir commands/core 33 | touch commands/core/ping.mjs 34 | ``` 35 | 36 | ```js 37 | export default { 38 | name: 'ping', 39 | description: 'Ping the bot', 40 | 41 | run(interaction: ChatInputInteraction) { 42 | interaction.reply({ 43 | content: 'Pong!', 44 | }); 45 | }, 46 | }; 47 | ``` 48 | 49 | This is the command itself. It's a simple ping command that replies with `Pong!` when you run it. 50 | -------------------------------------------------------------------------------- /apps/docs/docs/getting-started/framework-less/creating-main-file.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | import Tabs from '@theme/Tabs'; 6 | import TabItem from '@theme/TabItem'; 7 | 8 | # Creating the main file 9 | 10 | Firstly open your code editor and create a new file called `main.mjs` in a folder named "src" of your project. This is where we'll be writing our bot's code. 11 | 12 | 13 | 14 | 15 | 16 | ```js 17 | import { App } from 'disploy'; 18 | import { config as loadEnv } from 'std/dotenv/mod.ts'; 19 | import commands from './commands/commands.mjs'; 20 | 21 | await loadEnv({ 22 | export: true, 23 | }); 24 | ``` 25 | 26 | 27 | 28 | 29 | 30 | ```js 31 | import 'dotenv/config'; 32 | import { App } from 'disploy'; 33 | import commands from './commands/commands.mjs'; 34 | ``` 35 | 36 | 37 | 38 | 39 | 40 | We start by importing the `App` class from Disploy. We also import the `loadEnv` function from `std/dotenv/mod.ts` (Deno) or `dotenv` (Node.js). This function will load our environment variables from a `.env` file. 41 | 42 | 43 | 44 | 45 | 46 | ```js 47 | const clientId = Deno.env.get('DISCORD_CLIENT_ID'); 48 | const token = Deno.env.get('DISCORD_TOKEN'); 49 | const publicKey = Deno.env.get('DISCORD_PUBLIC_KEY'); 50 | ``` 51 | 52 | 53 | 54 | 55 | 56 | ```js 57 | const clientId = process.env.DISCORD_CLIENT_ID; 58 | const token = process.env.DISCORD_TOKEN; 59 | const publicKey = process.env.DISCORD_PUBLIC_KEY; 60 | ``` 61 | 62 | 63 | 64 | 65 | 66 | ```js 67 | if (!clientId || !token || !publicKey) { 68 | throw new Error('Missing environment variables'); 69 | } 70 | ``` 71 | 72 | We then create a new instance of the `App` class and export it so we can use it in other files. We then call the `start` method on the `app` instance and pass in our client ID, token and public key. 73 | 74 | ```js 75 | export const app = new App({ 76 | logger: { 77 | debug: true, 78 | }, 79 | }); 80 | 81 | app.start({ 82 | clientId, 83 | token, 84 | publicKey, 85 | }); 86 | ``` 87 | 88 | We then loop through our commands and register them with the `app.commands.registerCommand` method. We also set the `debug` option to `true` in the `logger` option of the `App` class. This helps us debug our bot. 89 | 90 | ```js {7-9} 91 | export const app = new App({ 92 | logger: { 93 | debug: true, 94 | }, 95 | }); 96 | 97 | for (const command of commands) { 98 | app.commands.registerCommand(command); 99 | } 100 | ``` 101 | -------------------------------------------------------------------------------- /apps/docs/docs/getting-started/framework-less/deploying-commands.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Deploying commands 6 | 7 | We can call `syncCommands` on our `CommandManager` attached to our `App` instance to deploy our commands to Discord. 8 | 9 | ```js 10 | import { app } from './main.mjs'; 11 | 12 | console.log(`Deploying ${app.commands.getCommands().size} commands...`); 13 | 14 | app.commands.syncCommands(false); // false = replace existing commands deployed to Discord 15 | 16 | console.log('Deployed!'); 17 | ``` 18 | -------------------------------------------------------------------------------- /apps/docs/docs/getting-started/framework-less/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Without the CLI Framework 6 | 7 | Using the Disploy CLI is completely optional. Its purpose is to bundle your bot into a single file that can be [deployed to Cloudflare Workers](/docs/getting-started/cli-framework/learning-the-workflow/deploying) or imported into your own project. 8 | -------------------------------------------------------------------------------- /apps/docs/docs/getting-started/framework-less/prep-work.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | import Tabs from '@theme/Tabs'; 6 | import TabItem from '@theme/TabItem'; 7 | 8 | # Preparations 9 | 10 | :::info 11 | 12 | We expect you to already have a working installation of Deno 1.28+ or Node.js 18+ 13 | 14 | ::: 15 | 16 | ## Setting up your project 17 | 18 | ### Dependencies 19 | 20 | 21 | 22 | 23 | 24 | Start by creating a `deno.json` file in your project directory: 25 | 26 | ```json 27 | { 28 | "importMap": "import_map.json" 29 | } 30 | ``` 31 | 32 | Then create an `import_map.json` file in the same directory: 33 | 34 | ```json 35 | { 36 | "imports": { 37 | "disploy": "npm:disploy@dev", 38 | "std/": "https://deno.land/std@0.164.0/", 39 | "fmt/": "https://deno.land/std@0.164.0/fmt/" 40 | } 41 | } 42 | ``` 43 | 44 | This will allow you to import Disploy from the `disploy` module. 45 | We are also importing the standard library from Deno's official repository. 46 | 47 | 48 | 49 | 50 | 51 | You can initialize a Node.js project with the following command: 52 | 53 | ```bash 54 | npm init -y 55 | # or 56 | yarn init -y 57 | # or 58 | pnpm init -y 59 | ``` 60 | 61 | Then install Disploy, dotenv and express since we will be using them in this guide: 62 | 63 | ```bash 64 | npm install disploy dotenv express 65 | # or 66 | yarn add disploy dotenv express 67 | # or 68 | pnpm add disploy dotenv express 69 | ``` 70 | 71 | 72 | 73 | 74 | 75 | ### Creating a `.env` 76 | 77 | Create a `.env` file in your project directory and add the following content: 78 | 79 | ```env 80 | DISCORD_CLIENT_ID="Your Discord bot's client id" 81 | DISCORD_TOKEN="Your Discord bot's token" 82 | DISCORD_PUBLIC_KEY="Your Discord's bot's public key" 83 | ``` 84 | 85 | You can find all of these values in your [Discord Developer Portal](https://discord.com/developers/applications). 86 | 87 | ![Public Key and Client ID](https://cdn.discordapp.com/attachments/1038456602610651196/1042198788653195306/Screenshot_2022-11-16_at_9.05.23_am.png) 88 | ![Bot Token](https://cdn.discordapp.com/attachments/1038456602610651196/1042198948938530977/Screenshot_2022-11-16_at_9.06.01_am.png) 89 | -------------------------------------------------------------------------------- /apps/docs/docs/getting-started/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Getting started 6 | 7 | Firstly before we do anything, we are going to need to bootstrap our project with or without the framework. 8 | 9 | ## Should I use the framework? 10 | 11 | - Easy to prototype and deploy ([`disploy dev`](/docs/getting-started/cli-framework/learning-the-workflow/dev-mode) and [`disploy deploy`](/docs/getting-started/cli-framework/learning-the-workflow/deploying)) 12 | - Serverless deployment out of the box ([`disploy deploy`](/docs/getting-started/cli-framework/learning-the-workflow/deploying)) 13 | - Easy to scale 14 | - Easy to maintain 15 | 16 | ## Should I not use the framework? 17 | 18 | - You want to use your own deployment method 19 | - You want to not be forced into serverless environments 20 | - You want to use `@disploy/ws` allowing you to receive events and use voice 21 | 22 | ## Bootstrap your project 23 | 24 | - [Without the CLI Framework](/docs/getting-started/framework-less) 25 | - [With the CLI Framework](/docs/category/cli-framework) 26 | -------------------------------------------------------------------------------- /apps/docs/docs/introduction.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 0 3 | --- 4 | 5 | # Introduction 6 | 7 | ## What is Disploy? 8 | 9 | Disploy handles interactions and transforms them into classes that you can use to easily interact with the Discord API. To put it simply, **Disploy is an interaction router**. 10 | 11 | We also have a CLI that makes it easy to quickly prototype and deploy your bot, it's simple but effective. It has a Next.js like pattern with a file system based router, and it's easy to deploy your bot to a Cloudflare worker and more with `disploy deploy`. You can learn more about the CLI in the [CLI guide](/docs/category/cli-framework). 12 | 13 | ## Why use Disploy? 14 | 15 | Disploy's main goal is to make it easy to build, test and deploy Discord bots. We want to make it easy to build bots that are easy to maintain and scale through serverless platforms like Cloudflare Workers. Disploy is extremely flexible and can be used on many different runtimes, including Cloudflare Workers, Vercel, Deno, Node.js and more. 16 | -------------------------------------------------------------------------------- /apps/docs/docusaurus.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | // Note: type annotations allow type checking and IDEs autocompletion 3 | 4 | const lightCodeTheme = require('prism-react-renderer/themes/vsLight'); 5 | const darkCodeTheme = require('prism-react-renderer/themes/vsDark'); 6 | 7 | /** @type {import('@docusaurus/types').Config} */ 8 | const config = { 9 | title: 'Disploy', 10 | tagline: 'Flexible router for building HTTP interaction-based Discord bots with ease.', 11 | url: 'https://disploy.dev', 12 | baseUrl: '/', 13 | onBrokenLinks: 'warn', 14 | onBrokenMarkdownLinks: 'warn', 15 | favicon: 'img/favicon.ico', 16 | 17 | // GitHub pages deployment config. 18 | // If you aren't using GitHub pages, you don't need these. 19 | organizationName: 'disploy', // Usually your GitHub org/user name. 20 | projectName: 'disploy', // Usually your repo name. 21 | 22 | // Even if you don't use internalization, you can use this field to set useful 23 | // metadata like html lang. For example, if your site is Chinese, you may want 24 | // to replace "en" with "zh-Hans". 25 | i18n: { 26 | defaultLocale: 'en', 27 | locales: ['en'], 28 | }, 29 | 30 | presets: [ 31 | [ 32 | 'classic', 33 | /** @type {import('@docusaurus/preset-classic').Options} */ 34 | ({ 35 | docs: { 36 | sidebarPath: require.resolve('./sidebars.js'), 37 | editUrl: 'https://github.com/disploy/Disploy/tree/main/apps/docs/', 38 | }, 39 | theme: { 40 | customCss: require.resolve('./src/css/custom.css'), 41 | }, 42 | }), 43 | ], 44 | ], 45 | 46 | plugins: [ 47 | async function tailwind(context, options) { 48 | return { 49 | name: 'docusaurus-tailwindcss', 50 | configurePostCss(postcssOptions) { 51 | postcssOptions.plugins.push(require('tailwindcss')); 52 | postcssOptions.plugins.push(require('autoprefixer')); 53 | return postcssOptions; 54 | }, 55 | }; 56 | }, 57 | [ 58 | 'docusaurus-plugin-typedoc', 59 | 60 | // Plugin / TypeDoc options 61 | { 62 | id: 'disploy', 63 | entryPoints: ['../../packages/disploy/src/index.ts'], 64 | tsconfig: '../../packages/disploy/tsconfig.json', 65 | out: 'Documentation/disploy', 66 | sidebar: { 67 | categoryLabel: 'disploy', 68 | position: 1, 69 | fullNames: true, 70 | }, 71 | }, 72 | ], 73 | ], 74 | 75 | themeConfig: 76 | /** @type {import('@docusaurus/preset-classic').ThemeConfig} */ 77 | ({ 78 | navbar: { 79 | title: 'Disploy', 80 | logo: { 81 | alt: 'Disploy logo', 82 | src: 'img/logo.svg', 83 | }, 84 | items: [ 85 | { 86 | type: 'doc', 87 | docId: 'introduction', 88 | position: 'left', 89 | label: 'Guides', 90 | }, 91 | { 92 | href: 'https://github.com/disploy/Disploy', 93 | label: 'GitHub', 94 | position: 'right', 95 | }, 96 | ], 97 | }, 98 | footer: { 99 | links: [ 100 | { 101 | title: 'Community', 102 | items: [ 103 | { 104 | label: 'Discord', 105 | href: 'https://discord.gg/E3z8MDnTWn', 106 | }, 107 | ], 108 | }, 109 | { 110 | title: 'More', 111 | items: [ 112 | { 113 | label: 'Guides', 114 | to: '/docs/introduction', 115 | }, 116 | { 117 | label: 'GitHub', 118 | href: 'https://github.com/disploy/Disploy', 119 | }, 120 | ], 121 | }, 122 | ], 123 | copyright: `Copyright © ${new Date().getFullYear()} Disploy. Built with Docusaurus.`, 124 | }, 125 | prism: { 126 | theme: lightCodeTheme, 127 | darkTheme: darkCodeTheme, 128 | }, 129 | }), 130 | }; 131 | 132 | module.exports = config; 133 | -------------------------------------------------------------------------------- /apps/docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@disploy/docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "dev": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "type-check": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "2.2.0", 19 | "@docusaurus/preset-classic": "2.2.0", 20 | "@heroicons/react": "^2.0.13", 21 | "@mdx-js/react": "^1.6.22", 22 | "clsx": "^1.2.1", 23 | "daisyui": "^2.46.0", 24 | "prism-react-renderer": "^1.3.5", 25 | "react": "^18.2.0", 26 | "react-dom": "^18.2.0", 27 | "tailwindcss": "^3.2.4" 28 | }, 29 | "devDependencies": { 30 | "@docusaurus/module-type-aliases": "2.2.0", 31 | "@tsconfig/docusaurus": "^1.0.6", 32 | "docusaurus-plugin-typedoc": "^0.18.0", 33 | "typedoc": "^0.23.23", 34 | "typedoc-plugin-markdown": "^3.14.0", 35 | "typescript": "^4.9.4" 36 | }, 37 | "browserslist": { 38 | "production": [ 39 | ">0.5%", 40 | "not dead", 41 | "not op_mini all" 42 | ], 43 | "development": [ 44 | "last 1 chrome version", 45 | "last 1 firefox version", 46 | "last 1 safari version" 47 | ] 48 | }, 49 | "engines": { 50 | "node": ">=16.14" 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /apps/docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | module.exports = sidebars; 34 | -------------------------------------------------------------------------------- /apps/docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /** 6 | * Any CSS included here will be global. The classic template 7 | * bundles Infima by default. Infima is a CSS framework designed to 8 | * work well for content-centric websites. 9 | */ 10 | 11 | /* You can override the default Infima variables here. */ 12 | :root { 13 | --ifm-color-primary: #5865f2; 14 | --ifm-color-primary-dark: #748ad8; 15 | --ifm-color-primary-darker: #748ad8; 16 | --ifm-color-primary-darkest: #748ad8; 17 | --ifm-color-primary-light: #5865f2; 18 | --ifm-color-primary-lighter: #5865f2; 19 | --ifm-color-primary-lightest: #5865f2; 20 | --ifm-code-font-size: 95%; 21 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 22 | --ifm-footer-background-color: #23272a; 23 | } 24 | 25 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 26 | [data-theme='dark'] { 27 | --ifm-color-primary: #5865f2; 28 | --ifm-color-primary-dark: #748ad8; 29 | --ifm-color-primary-darker: #748ad8; 30 | --ifm-color-primary-darkest: #748ad8; 31 | --ifm-color-primary-light: #5865f2; 32 | --ifm-color-primary-lighter: #5865f2; 33 | --ifm-color-primary-lightest: #5865f2; 34 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 35 | } 36 | -------------------------------------------------------------------------------- /apps/docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /apps/docs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import Link from '@docusaurus/Link'; 2 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 3 | import CodeBlock from '@theme/CodeBlock'; 4 | import Layout from '@theme/Layout'; 5 | import React from 'react'; 6 | 7 | const codeSnippet = `import type { Command } from 'disploy'; 8 | 9 | export default { 10 | name: 'ping', 11 | description: 'pong!', 12 | 13 | async run(interaction) { 14 | return void interaction.reply({ 15 | content: 'ok', 16 | }) 17 | } 18 | } satisfies Command;`; 19 | 20 | function HomepageHeader() { 21 | const { siteConfig } = useDocusaurusContext(); 22 | 23 | return ( 24 |
25 |
26 | {codeSnippet} 27 |
28 |

Disploy

29 |

{siteConfig.tagline}

30 | 31 | Get started! 32 | 33 |
34 |
35 |
36 | ); 37 | } 38 | 39 | export default function Home(): JSX.Element { 40 | return ( 41 | 42 | 43 | 44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /apps/docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /apps/docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disploy/disploy/78c947745b2e83f792e51f7251f0e5edf9395028/apps/docs/static/.nojekyll -------------------------------------------------------------------------------- /apps/docs/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /apps/docs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: ['./src/**/*.{js,jsx,ts,tsx}'], 3 | theme: { 4 | extend: {}, 5 | }, 6 | darkMode: ['class', '[data-theme="dark"]'], 7 | plugins: [require('daisyui')], 8 | corePlugins: { 9 | preflight: false, 10 | }, 11 | daisyui: { 12 | themes: [ 13 | { 14 | dark: { 15 | primary: '#5865F2', 16 | secondary: '#748ad8', 17 | accent: '#eb459e', 18 | neutral: '#5865F2', 19 | 'base-100': '#23272A', 20 | info: '#5865F2', 21 | success: '#4b5bab', 22 | warning: '#f2a65e', 23 | error: '#b0305c', 24 | }, 25 | light: { 26 | primary: '#5865F2', 27 | secondary: '#748ad8', 28 | accent: '#eb459e', 29 | neutral: '#5865F2', 30 | 'base-100': '#fff', 31 | info: '#5865F2', 32 | success: '#4b5bab', 33 | warning: '#f2a65e', 34 | error: '#b0305c', 35 | }, 36 | }, 37 | ], 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /apps/docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@tsconfig/docusaurus/tsconfig.json", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /apps/example/README.md: -------------------------------------------------------------------------------- 1 | # `@disploy/example` 2 | 3 | This is an example Discord bot made with Disploy to test the library. 4 | 5 | ## Usage 6 | 7 | You should be already running `yarn dev` in the root of the repository (live transpile TypeScript), then you can run this bot with: 8 | 9 | ```bash 10 | yarn workspace @disploy/example start --sync # Sync will register commands with Discord, you should only need to add this flag once, or when you add new commands 11 | ``` 12 | 13 | Next you will need to make the interaction server available to Discord. You can do this by using a service like [ngrok](https://ngrok.com/) or [Cloudflare tunnel](https://developers.cloudflare.com/cloudflare-one/connections/connect-apps/). 14 | 15 | ```bash 16 | ngrok http 3000 17 | ``` 18 | 19 | Set the `INTERACTIONS ENDPOINT URL` to https://xxxx-xx-xxx-xxx-xx.ngrok.io/interaction in the Discord Developer Portal. 20 | 21 | ### Required environment variables 22 | 23 | - `DISCORD_CLIENT_ID`: The Discord application client ID 24 | - `DISCORD_TOKEN`: The Discord bot token 25 | - `DISCORD_PUBLIC_KEY`: The Discord application public key 26 | 27 | We recommend using [env-cmd](https://www.npmjs.com/package/env-cmd) to load environment variables from a `.env` file. 28 | -------------------------------------------------------------------------------- /apps/example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@disploy/example", 3 | "version": "0.0.0", 4 | "license": "Apache-2.0", 5 | "main": "./dist/index.js", 6 | "source": "./src/index.ts", 7 | "types": "./dist/index.d.ts", 8 | "type": "module", 9 | "files": [ 10 | "dist" 11 | ], 12 | "contributors": [ 13 | "Tristan Camejo " 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Disploy/disploy.git" 18 | }, 19 | "homepage": "https://disploy.dev", 20 | "scripts": { 21 | "build": "tsup", 22 | "dev": "tsup --watch", 23 | "start": "node ./dist/index.js", 24 | "type-check": "tsc --noEmit", 25 | "lint": "TIMING=1 eslint src/**/*.ts* --fix" 26 | }, 27 | "dependencies": { 28 | "@discordjs/opus": "^0.9.0", 29 | "@discordjs/voice": "^0.14.0", 30 | "@disploy/ws": "workspace:^", 31 | "discord-api-types": "^0.37.24", 32 | "disploy": "workspace:^", 33 | "express": "^4.18.2", 34 | "glob": "^8.0.3", 35 | "tweetnacl": "^1.0.3" 36 | }, 37 | "devDependencies": { 38 | "@types/express": "^4.17.15", 39 | "@types/glob": "^8.0.0", 40 | "@types/node": "^18.11.17", 41 | "eslint": "8.30.0", 42 | "eslint-config-custom": "workspace:^", 43 | "rimraf": "^3.0.2", 44 | "tsconfig": "workspace:^", 45 | "typescript": "^4.9.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /apps/example/src/commands/ping.ts: -------------------------------------------------------------------------------- 1 | import { MessageFlags } from 'discord-api-types/v10'; 2 | import type { ChatInputCommand, ChatInputInteraction } from 'disploy'; 3 | 4 | export default new (class PingCommand implements ChatInputCommand { 5 | public name = 'ping'; 6 | public description = 'Ping the bot'; 7 | public options = []; 8 | 9 | public async run(interaction: ChatInputInteraction) { 10 | interaction.reply({ content: 'ok', flags: MessageFlags.Ephemeral }); 11 | } 12 | })(); 13 | -------------------------------------------------------------------------------- /apps/example/src/commands/voice-test.ts: -------------------------------------------------------------------------------- 1 | import { AudioPlayer, createAudioResource, joinVoiceChannel } from '@discordjs/voice'; 2 | import { APIApplicationCommandOption, ApplicationCommandOptionType, MessageFlags } from 'discord-api-types/v10'; 3 | import type { ChatInputCommand, ChatInputInteraction } from 'disploy'; 4 | import path from 'node:path'; 5 | 6 | export default new (class VoiceTestCommand implements ChatInputCommand { 7 | public name = 'voice-test'; 8 | public description = 'Voice test 123'; 9 | public options: APIApplicationCommandOption[] = [ 10 | { 11 | name: 'channel', 12 | description: 'The channel to join', 13 | type: ApplicationCommandOptionType.String, 14 | required: true, 15 | }, 16 | ]; 17 | 18 | public async run(interaction: ChatInputInteraction) { 19 | if (!interaction.guild) { 20 | return void interaction.reply({ 21 | content: 'This command can only be used in a guild', 22 | flags: MessageFlags.Ephemeral, 23 | }); 24 | } 25 | 26 | interaction.deferReply(); 27 | 28 | const channel = interaction.options.getString('channel'); 29 | 30 | const connection = joinVoiceChannel({ 31 | channelId: channel, 32 | guildId: interaction.guild.id, 33 | adapterCreator: interaction.app.ws.createVoiceAdapter(interaction.guild.id), 34 | }); 35 | 36 | const player = new AudioPlayer(); 37 | const song = createAudioResource(path.join(__dirname, '..', '..', 'music', 'audio.mp3')); 38 | 39 | player.play(song); 40 | connection.subscribe(player); 41 | 42 | return void interaction.editReply({ 43 | content: `ok`, 44 | }); 45 | } 46 | })(); 47 | -------------------------------------------------------------------------------- /apps/example/src/events/message-ping.ts: -------------------------------------------------------------------------------- 1 | import type { EventHandler } from '../types/EventHandler'; 2 | 3 | const MessagePing: EventHandler<'messageCreate'> = { 4 | event: 'messageCreate', 5 | handle: async (message) => { 6 | if (message.content === '!ping') { 7 | const msg = await message.reply({ content: 'ok?' }); 8 | 9 | msg.edit({ content: `ok @ ${msg.timestamp - message.timestamp}ms` }); 10 | } 11 | }, 12 | }; 13 | 14 | export default MessagePing; 15 | -------------------------------------------------------------------------------- /apps/example/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Gateway } from '@disploy/ws'; 2 | import { GatewayIntentBits } from 'discord-api-types/v10'; 3 | import { App, Command, expressAdapter, MessageComponentHandler } from 'disploy'; 4 | import express from 'express'; 5 | import glob from 'glob'; 6 | import path from 'path'; 7 | import type { EventHandler } from './types/EventHandler'; 8 | 9 | // Handle environment variables 10 | // recommend(tristan): use env-cmd to load a .env file 11 | const clientId = process.env.DISCORD_CLIENT_ID; 12 | const token = process.env.DISCORD_TOKEN; 13 | const publicKey = process.env.DISCORD_PUBLIC_KEY; 14 | 15 | if (!clientId || !token || !publicKey) { 16 | throw new Error('Missing environment variables'); 17 | } 18 | 19 | // Setup Discord application 20 | const app = new App(); 21 | 22 | app.start({ 23 | clientId, 24 | token, 25 | publicKey, 26 | }); 27 | 28 | // Load commands 29 | glob(path.join(__dirname, 'commands', '**', '*.js').replaceAll('\\', '/'), async (err, files) => { 30 | if (err) { 31 | throw err; 32 | } 33 | 34 | await Promise.all( 35 | files.map(async (file) => { 36 | const command = (await import(file)).default as Command; 37 | 38 | app.commands.registerCommand(command); 39 | }), 40 | ); 41 | }); 42 | 43 | if (process.argv.includes('--sync') || process.argv.includes('--sync-merge')) { 44 | app.commands.syncCommands(process.argv.includes('--sync-merge')); 45 | } 46 | 47 | // Load message components 48 | glob(path.join(__dirname, 'handlers', '**', '*.js').replaceAll('\\', '/'), async (err, files) => { 49 | if (err) { 50 | throw err; 51 | } 52 | 53 | await Promise.all( 54 | files.map(async (file) => { 55 | const handler = (await import(file)).default as MessageComponentHandler; 56 | 57 | app.handlers.registerHandler(handler); 58 | }), 59 | ); 60 | }); 61 | 62 | // Setup gateway connection 63 | app.ws = new Gateway(app, { 64 | intents: GatewayIntentBits.MessageContent | GatewayIntentBits.GuildMessages | GatewayIntentBits.GuildVoiceStates, 65 | }); 66 | 67 | // Load event handlers 68 | glob(path.join(__dirname, 'events', '**', '*.js').replaceAll('\\', '/'), async (err, files) => { 69 | if (err) { 70 | throw err; 71 | } 72 | 73 | await Promise.all( 74 | files.map(async (file) => { 75 | const handler = (await import(file)).default as EventHandler; 76 | 77 | app.ws.on(handler.event, handler.handle); 78 | }), 79 | ); 80 | }); 81 | 82 | // Setup interaction server 83 | const interactionServer = express(); 84 | 85 | interactionServer.use(express.json()); 86 | expressAdapter(app, interactionServer); 87 | 88 | interactionServer.listen(3000, () => { 89 | console.log('[interaction server] Listening on port 3000'); 90 | }); 91 | 92 | // Connect to gateway 93 | app.ws.connect(); 94 | 95 | // Types 96 | declare module 'disploy' { 97 | interface App { 98 | ws: Gateway; 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /apps/example/src/types/EventHandler.ts: -------------------------------------------------------------------------------- 1 | import type { GatewayEvents } from '@disploy/ws'; 2 | 3 | export interface EventHandler { 4 | event: T; 5 | handle: (...args: GatewayEvents[T]) => void; 6 | } 7 | -------------------------------------------------------------------------------- /apps/example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "include": ["./src"], 4 | "compilerOptions": { 5 | "outDir": "./dist" 6 | }, 7 | "exclude": ["dist", "build", "node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /apps/example/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { createTsupConfig } from '../../tsup.config.js'; 2 | 3 | export default createTsupConfig({ 4 | entry: ['src/**/*.ts', '!src/**/*.d.ts', 'src/**/*.tsx'], 5 | format: ['esm'], 6 | tsconfig: 'src/tsconfig.json', 7 | }); 8 | -------------------------------------------------------------------------------- /apps/framework-example/README.md: -------------------------------------------------------------------------------- 1 | # `@disploy/framework-example` 2 | 3 | This is an example Discord bot made with Disploy to test the library. 4 | 5 | ## Usage 6 | 7 | You should be already running `yarn dev` in the root of the repository (live transpile TypeScript), then you can deploy this bot to a Cloudflare Worker with: 8 | 9 | ```bash 10 | yarn disploy deploy 11 | ``` 12 | 13 | or just run a local dev server with an ngrok tunnel: 14 | 15 | ```bash 16 | yarn disploy dev 17 | ``` 18 | 19 | Commands will be automatically registered when running `yarn disploy dev`, but you'll need to manually register them when deploying to a Cloudflare Worker (`yarn disploy sync`). 20 | -------------------------------------------------------------------------------- /apps/framework-example/disploy.json: -------------------------------------------------------------------------------- 1 | { 2 | "prebuild": "yarn run build", 3 | "root": "dist", 4 | "target": { 5 | "type": "cloudflare", 6 | "name": "cf-example" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /apps/framework-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@disploy/framework-example", 3 | "version": "0.0.0", 4 | "main": "./dist/index.js", 5 | "source": "./src/index.ts", 6 | "types": "./dist/index.d.ts", 7 | "scripts": { 8 | "build": "rimraf dist && tsc", 9 | "type-check": "tsc --noEmit", 10 | "dev": "tsc -w", 11 | "serve": "disploy dev", 12 | "test-server": "disploy test-server" 13 | }, 14 | "dependencies": { 15 | "discord-api-types": "^0.37.24", 16 | "disploy": "workspace:^" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "^18.11.17", 20 | "eslint": "8.30.0", 21 | "eslint-config-custom": "workspace:^", 22 | "rimraf": "^3.0.2", 23 | "tsconfig": "workspace:^", 24 | "typescript": "^4.9.4" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /apps/framework-example/src/commands/channel.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationCommandOptionType, ChannelType } from 'discord-api-types/v10'; 2 | import type { Command } from 'disploy'; 3 | 4 | export default { 5 | name: 'channel', 6 | description: 'fetch a channel!', 7 | options: [ 8 | { 9 | name: 'channel', 10 | description: 'the channel to fetch', 11 | type: ApplicationCommandOptionType.Channel, 12 | required: true, 13 | }, 14 | ], 15 | 16 | async run(interaction) { 17 | interaction.deferReply(); 18 | 19 | try { 20 | const channel = interaction.options.getChannel('channel'); 21 | 22 | let message = `${channel} is `; 23 | 24 | switch (channel.type) { 25 | case ChannelType.GuildVoice: 26 | message += 'a voice channel'; 27 | break; 28 | case ChannelType.GuildText: 29 | message += 'a text channel'; 30 | break; 31 | } 32 | 33 | return void interaction.editReply({ 34 | content: message, 35 | }); 36 | } catch (error) { 37 | const err = error as Error; 38 | return void interaction.editReply({ 39 | content: ['```js', err.stack ?? err.message, '```'].join('\n'), 40 | }); 41 | } 42 | }, 43 | } satisfies Command; 44 | -------------------------------------------------------------------------------- /apps/framework-example/src/commands/classtest.ts: -------------------------------------------------------------------------------- 1 | import type { ChatInputCommand, ChatInputInteraction } from 'disploy'; 2 | 3 | class TestClass implements ChatInputCommand { 4 | public name = 'class'; 5 | public description = 'this command is a class!'; 6 | 7 | private privateMethod() { 8 | return 'yo classes are sick'; 9 | } 10 | 11 | public async run(interaction: ChatInputInteraction) { 12 | const message = this.privateMethod(); 13 | 14 | return void interaction.reply({ 15 | content: message, 16 | }); 17 | } 18 | } 19 | export default new TestClass(); 20 | -------------------------------------------------------------------------------- /apps/framework-example/src/commands/env.ts: -------------------------------------------------------------------------------- 1 | import type { Command } from 'disploy'; 2 | 3 | export default { 4 | name: 'env', 5 | description: 'test disploy envs!', 6 | 7 | async run(interaction) { 8 | return void interaction.reply({ 9 | content: `The environment variable "TEST" is set to "${interaction.app.env.get('TEST') ?? 'undefined'}"`, 10 | }); 11 | }, 12 | } satisfies Command; 13 | -------------------------------------------------------------------------------- /apps/framework-example/src/commands/hey.ts: -------------------------------------------------------------------------------- 1 | import type { Command } from 'disploy'; 2 | 3 | export default { 4 | name: 'hey', 5 | description: 'heyy!', 6 | 7 | async run(interaction) { 8 | const reply = await interaction.deferReply({ fetchReply: true }); 9 | 10 | if (!interaction.guild) { 11 | return void interaction.editReply({ 12 | content: 'You must use this in a guild.', 13 | }); 14 | } 15 | 16 | const guild = await interaction.guild.fetch(); 17 | 18 | return void interaction.editReply({ 19 | content: `Hello the people of ${guild.name}! Fun fact! The id of this message is \`${reply.id}\`!`, 20 | }); 21 | }, 22 | } satisfies Command; 23 | -------------------------------------------------------------------------------- /apps/framework-example/src/commands/image-test.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationCommandOptionType, MessageFlags } from 'discord-api-types/v10'; 2 | import type { ChatInputInteraction, Command } from 'disploy'; 3 | 4 | export default { 5 | name: 'image-test', 6 | description: 'test an image', 7 | options: [ 8 | { 9 | name: 'image', 10 | description: 'your image', 11 | type: ApplicationCommandOptionType.Attachment, 12 | required: true, 13 | }, 14 | ], 15 | 16 | async run(interaction: ChatInputInteraction) { 17 | const attachment = interaction.options.getAttachment('image'); 18 | 19 | if (attachment.contentType !== 'image/png') { 20 | return void interaction.reply({ 21 | content: 'that is not a png!', 22 | flags: MessageFlags.Ephemeral, 23 | }); 24 | } 25 | 26 | return void interaction.reply({ 27 | embeds: [ 28 | { 29 | title: 'your image', 30 | image: { 31 | url: attachment.url, 32 | }, 33 | }, 34 | ], 35 | }); 36 | }, 37 | } satisfies Command; 38 | -------------------------------------------------------------------------------- /apps/framework-example/src/commands/me.ts: -------------------------------------------------------------------------------- 1 | import type { ChatInputInteraction, Command } from 'disploy'; 2 | 3 | export default { 4 | name: 'me', 5 | description: 'fetch me!', 6 | async run(interaction: ChatInputInteraction) { 7 | interaction.deferReply(); 8 | 9 | try { 10 | const user = await interaction.app.user.fetch(); 11 | 12 | return void interaction.editReply({ 13 | embeds: [ 14 | { 15 | title: 'User', 16 | description: `**Username**: ${user.username}\n**Discriminator**: ${user.discriminator}\n**ID**: ${user.id}`, 17 | }, 18 | ], 19 | }); 20 | } catch (error) { 21 | const err = error as Error; 22 | return void interaction.editReply({ 23 | content: ['```js', err.stack ?? err.message, '```'].join('\n'), 24 | }); 25 | } 26 | }, 27 | } satisfies Command; 28 | -------------------------------------------------------------------------------- /apps/framework-example/src/commands/member.ts: -------------------------------------------------------------------------------- 1 | import { ApplicationCommandOptionType } from 'discord-api-types/v10'; 2 | import type { ChatInputInteraction, Command } from 'disploy'; 3 | 4 | export default { 5 | name: 'member', 6 | description: 'fetch a member in a guild!', 7 | options: [ 8 | { 9 | name: 'member', 10 | description: 'the user to fetch as a member', 11 | type: ApplicationCommandOptionType.User, 12 | required: true, 13 | }, 14 | ], 15 | 16 | async run(interaction: ChatInputInteraction) { 17 | interaction.deferReply(); 18 | if (!interaction.guild) { 19 | return void interaction.editReply({ 20 | content: 'run me in a guild!', 21 | }); 22 | } 23 | const partialMember = interaction.options.getUser('member'); 24 | const member = await (await interaction.guild.fetch()).members.fetch(partialMember.id).catch(() => null); 25 | if (!member) { 26 | return void interaction.editReply({ 27 | content: 'that user is not in this guild!', 28 | }); 29 | } 30 | 31 | return void interaction.editReply({ 32 | content: [ 33 | `**Tag**: ${member.user.tag}`, 34 | `**ID**: ${member.user.id}`, 35 | `**Username:** ${member.user.username}`, 36 | `**Discriminator**: ${member.user.discriminator}`, 37 | `**Nickname**: ${member.nickname ?? 'None'}`, 38 | `**Deafened:** ${member.deaf ? 'yes' : 'no'}`, 39 | `**Muted:** ${member.mute ? 'yes' : 'no'}`, 40 | `**Nickname:** ${member.nickname ?? 'none'}`, 41 | `**Joined At:** ${member.joinedAt}`, 42 | ].join('\n'), 43 | }); 44 | }, 45 | } satisfies Command; 46 | -------------------------------------------------------------------------------- /apps/framework-example/src/commands/ping.ts: -------------------------------------------------------------------------------- 1 | import { ButtonStyle, ComponentType } from 'discord-api-types/v10'; 2 | import type { Command } from 'disploy'; 3 | 4 | export default { 5 | name: 'ping', 6 | description: 'pong!', 7 | 8 | async run(interaction) { 9 | const reply = await interaction.deferReply({ fetchReply: true }); 10 | 11 | return void interaction.editReply({ 12 | content: `ok (in ${reply.timestamp - interaction.createdTimestamp}ms)`, 13 | components: [ 14 | { 15 | type: ComponentType.ActionRow, 16 | components: [ 17 | { 18 | type: ComponentType.Button, 19 | label: 'Click me!', 20 | style: ButtonStyle.Primary, 21 | custom_id: `ping-${interaction.user.id}`, 22 | }, 23 | { 24 | type: ComponentType.Button, 25 | label: 'i have no params', 26 | style: ButtonStyle.Secondary, 27 | emoji: { 28 | name: '🫢', 29 | }, 30 | custom_id: `ping`, 31 | }, 32 | ], 33 | }, 34 | ], 35 | }); 36 | }, 37 | } satisfies Command; 38 | -------------------------------------------------------------------------------- /apps/framework-example/src/handlers/ping.ts: -------------------------------------------------------------------------------- 1 | import type { ButtonHandler } from 'disploy'; 2 | 3 | export default { 4 | customId: 'ping-:userId', 5 | 6 | async run(interaction) { 7 | const originalUser = await interaction.params.getUserParam('userId'); 8 | const clicker = interaction.user; 9 | 10 | return void interaction.reply({ 11 | content: `hello world!!!!!!!! (clicked by ${clicker}) [made by ${originalUser}]`, 12 | allowed_mentions: { 13 | users: [], 14 | }, 15 | }); 16 | }, 17 | } satisfies ButtonHandler; 18 | -------------------------------------------------------------------------------- /apps/framework-example/src/handlers/pingNoParams.ts: -------------------------------------------------------------------------------- 1 | import type { ButtonHandler } from 'disploy'; 2 | 3 | export default { 4 | customId: 'ping', 5 | 6 | async run(interaction) { 7 | const reply = await interaction.reply( 8 | { 9 | content: `hello world!!!!!!!! (clicked by ${interaction.user})`, 10 | allowed_mentions: { 11 | users: [], 12 | }, 13 | }, 14 | true, 15 | ); 16 | 17 | interaction.followUp({ 18 | content: `this is a followup message for [this interaction](${reply.url()}) it took ${ 19 | reply.timestamp - interaction.createdTimestamp 20 | }ms to send`, 21 | }); 22 | }, 23 | } satisfies ButtonHandler; 24 | -------------------------------------------------------------------------------- /apps/framework-example/src/index.ts: -------------------------------------------------------------------------------- 1 | // :) 2 | -------------------------------------------------------------------------------- /apps/framework-example/src/lib/test.ts: -------------------------------------------------------------------------------- 1 | export const testImportVar = 'hello world!'; 2 | -------------------------------------------------------------------------------- /apps/framework-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "include": ["./src"], 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "declarationMap": false, 7 | "declaration": false, 8 | "sourceMap": false, 9 | "inlineSources": false 10 | }, 11 | "exclude": ["dist", "node_modules"] 12 | } 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@disploy/disploy", 3 | "version": "0.0.0", 4 | "private": true, 5 | "workspaces": [ 6 | "apps/*", 7 | "packages/*" 8 | ], 9 | "scripts": { 10 | "build": "turbo run build --filter=!@disploy/docs", 11 | "build:docs": "turbo run build --filter=@disploy/docs", 12 | "test": "turbo run test", 13 | "type-check": "turbo run type-check --parallel", 14 | "dev": "turbo run dev --parallel --filter=!@disploy/docs", 15 | "dev:docs": "turbo run dev --parallel --filter=@disploy/docs", 16 | "lint": "turbo run lint", 17 | "format": "prettier --write \"**/*.{ts,tsx,md}\"", 18 | "publish": "node scripts/ayumi.mjs publish", 19 | "publish:dev": "node scripts/ayumi.mjs publish -d", 20 | "benchmark": "node scripts/benchmark.mjs" 21 | }, 22 | "devDependencies": { 23 | "@jest/globals": "^29.3.1", 24 | "disbench": "^2.0.0", 25 | "eslint-config-custom": "workspace:^", 26 | "prettier": "^2.8.1", 27 | "tsup": "^6.5.0", 28 | "turbo": "^1.6.3", 29 | "typescript": "^4.9.4" 30 | }, 31 | "engines": { 32 | "npm": ">=7.0.0", 33 | "node": ">=14.0.0" 34 | }, 35 | "packageManager": "yarn@3.3.0" 36 | } 37 | -------------------------------------------------------------------------------- /packages/disploy/.cliff-jumperrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json", 3 | "name": "disploy", 4 | "packagePath": "packages/disploy", 5 | "tagTemplate": "{{new-version}}" 6 | } 7 | -------------------------------------------------------------------------------- /packages/disploy/CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disploy/disploy/78c947745b2e83f792e51f7251f0e5edf9395028/packages/disploy/CHANGELOG.md -------------------------------------------------------------------------------- /packages/disploy/cli/assets/code/array.js: -------------------------------------------------------------------------------- 1 | {{imports}} 2 | 3 | export const {{name}} = [ 4 | {{array}} 5 | ]; 6 | -------------------------------------------------------------------------------- /packages/disploy/cli/assets/code/cfWorkerEntry.js: -------------------------------------------------------------------------------- 1 | import { App } from 'disploy'; 2 | import { Commands } from './Commands'; 3 | import { Handlers } from './Handlers'; 4 | 5 | function createCloudflareAdapter(app) { 6 | return async function (req, randId) { 7 | if (req.method !== 'POST') { 8 | return new Response('Method not allowed', { status: 405 }); 9 | } 10 | let reqHeaders = {}; 11 | for (const [key, value] of req.headers) { 12 | reqHeaders[key] = value; 13 | } 14 | const tReq = { 15 | body: await req.json(), 16 | headers: reqHeaders, 17 | _request: req, 18 | randId, 19 | }; 20 | const payload = await app.router.entry(tReq); 21 | const { status, headers, body } = payload.serialized; 22 | return new Response(JSON.stringify(body), { 23 | status, 24 | headers: (() => { 25 | const headersObj = { 26 | 'content-type': 'application/json', 27 | }; 28 | for (const [key, value] of Object.entries(headers)) { 29 | if (typeof value === 'string') { 30 | headersObj[key] = value; 31 | } 32 | if (Array.isArray(value)) { 33 | headersObj[key] = value.join(','); 34 | } 35 | } 36 | return headersObj; 37 | })(), 38 | }); 39 | }; 40 | } 41 | 42 | export default { 43 | async fetch(request, env, ctx) { 44 | const app = new App({ commands: Commands, handlers: Handlers, env }); 45 | app.start({ 46 | publicKey: env.PUBLIC_KEY, 47 | clientId: env.CLIENT_ID, 48 | token: env.TOKEN, 49 | }); 50 | const randId = Math.random().toString(36).substring(7); 51 | ctx.waitUntil( 52 | new Promise((resolve) => { 53 | app.router.on(`finish-${randId}`, () => { 54 | resolve(); 55 | }); 56 | }), 57 | ); 58 | return await createCloudflareAdapter(app)(request, randId); 59 | }, 60 | }; 61 | -------------------------------------------------------------------------------- /packages/disploy/cli/assets/code/devServerEntry.js: -------------------------------------------------------------------------------- 1 | import { App } from 'disploy'; 2 | import { Commands } from './Commands'; 3 | import { Handlers } from './Handlers'; 4 | 5 | const app = new App({ 6 | commands: Commands, 7 | handlers: Handlers, 8 | env: process.env ?? Deno.env.toObject(), 9 | logger: { 10 | debug: true, 11 | }, 12 | }); 13 | 14 | export default { app, commands: Commands, handlers: Handlers }; 15 | -------------------------------------------------------------------------------- /packages/disploy/cli/assets/code/standaloneEntry.js: -------------------------------------------------------------------------------- 1 | import { App } from 'disploy'; 2 | import { Commands } from './Commands'; 3 | import { Handlers } from './Handlers'; 4 | 5 | const app = new App({ 6 | commands: Commands, 7 | handlers: Handlers, 8 | env: process.env ?? Deno.env.toObject(), 9 | }); 10 | 11 | export default { app, commands: Commands, handlers: Handlers }; 12 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/Constants.ts: -------------------------------------------------------------------------------- 1 | export const StartersEndpoint = 'https://raw.githubusercontent.com/Disploy/starters/main/starters.json'; 2 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/commands/build.ts: -------------------------------------------------------------------------------- 1 | import type { Argv, CommandModule } from 'yargs'; 2 | import { BuildApp } from './common/build'; 3 | 4 | export const aliases: string[] = []; 5 | export const desc: string = 'Build a project'; 6 | 7 | export const builder = (yargs: Argv) => 8 | yargs.options('skip-prebuild', { 9 | type: 'boolean', 10 | alias: 'sp', 11 | description: 'Skip the prebuild script', 12 | default: false, 13 | }); 14 | 15 | export const BuildCommand: CommandModule<{}, { 'skip-prebuild': boolean }> = { 16 | aliases, 17 | builder, 18 | command: 'build', 19 | async handler(opts) { 20 | await BuildApp({ 21 | skipPrebuild: opts.skipPrebuild, 22 | }); 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/commands/common/build.ts: -------------------------------------------------------------------------------- 1 | import * as color from 'colorette'; 2 | import ora from 'ora'; 3 | import { Compile } from '../../lib/compiler'; 4 | import type { DisployConfig } from '../../lib/disployConf'; 5 | import { ProjectTools } from '../../lib/ProjectTools'; 6 | import { runShellCommand } from '../../lib/shell'; 7 | import { UserError } from '../../lib/UserError'; 8 | 9 | export async function BuildApp({ 10 | skipPrebuild = false, 11 | overrideTarget, 12 | entryFileName = 'entry.mjs', 13 | }: 14 | | { 15 | skipPrebuild?: boolean; 16 | overrideTarget?: DisployConfig['target']; 17 | entryFileName?: string; 18 | } 19 | | undefined = {}) { 20 | let spinner = ora('Resolving project').start(); 21 | 22 | const { root, prebuild, target } = await ProjectTools.resolveProject({ 23 | cwd: process.cwd(), 24 | }); 25 | 26 | spinner.succeed(); 27 | 28 | if (prebuild && !skipPrebuild) { 29 | spinner = ora('Running prebuild script').start(); 30 | try { 31 | await runShellCommand(prebuild); 32 | } catch (err: any) { 33 | spinner.fail(); 34 | throw new UserError(`Prebuild script failed because ${err?.message || 'of an unknown error'}.`); 35 | } 36 | spinner.succeed(); 37 | } 38 | 39 | spinner = ora( 40 | ['Bundling project', `${color.gray('Target:')} ${color.magenta(overrideTarget?.type || target.type)}`].join('\n'), 41 | ).start(); 42 | const res = await Compile({ root, target: overrideTarget || target, entryFileName }); 43 | spinner.succeed(); 44 | return res; 45 | } 46 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/commands/deploy.ts: -------------------------------------------------------------------------------- 1 | import * as color from 'colorette'; 2 | import ora from 'ora'; 3 | import type { Argv, CommandModule } from 'yargs'; 4 | import { ProjectTools } from '../lib/ProjectTools'; 5 | import { UserError } from '../lib/UserError'; 6 | import { WranglerWrapper } from '../lib/WranglerWrapper'; 7 | import { logger } from '../utils/logger'; 8 | import { BuildApp } from './common/build'; 9 | 10 | export const aliases: string[] = []; 11 | export const desc: string = 'Build a project'; 12 | 13 | export const builder = (yargs: Argv) => 14 | yargs.options('skip-prebuild', { 15 | type: 'boolean', 16 | alias: 'sp', 17 | description: 'Skip the prebuild script', 18 | default: false, 19 | }); 20 | 21 | export const DeployCommand: CommandModule<{}, { 'skip-prebuild': boolean }> = { 22 | aliases, 23 | builder, 24 | command: 'deploy', 25 | async handler(opts) { 26 | const conf = await ProjectTools.resolveProject({ 27 | cwd: process.cwd(), 28 | }); 29 | const entry = await BuildApp({ 30 | skipPrebuild: opts.skipPrebuild, 31 | }); 32 | 33 | switch (conf.target.type) { 34 | case 'cloudflare': { 35 | const wrangler = new WranglerWrapper(conf, entry); 36 | 37 | const spinner = ora('Deploying to Cloudflare Workers').start(); 38 | 39 | await wrangler.publish(); 40 | 41 | spinner.succeed('Deployed to Cloudflare Workers'); 42 | 43 | logger.info( 44 | [ 45 | "You should visit the Cloudflare Workers dashboard to add your environment variables if you haven't already.", 46 | `${color.gray('CLIENT_ID')} Your Discord application's client ID`, 47 | `${color.gray('PUBLIC_KEY')} Your Discord application's public key`, 48 | `${color.gray('TOKEN')} Your Discord application's bot token`, 49 | ].join('\n'), 50 | ); 51 | break; 52 | } 53 | 54 | default: { 55 | throw new UserError(`Unsupported deploy target type: ${conf.target.type}`); 56 | } 57 | } 58 | }, 59 | }; 60 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/commands/dev.ts: -------------------------------------------------------------------------------- 1 | import chokidar from 'chokidar'; 2 | import * as color from 'colorette'; 3 | import { resolve } from 'import-meta-resolve'; 4 | import * as ngrok from 'ngrok'; 5 | import path from 'node:path'; 6 | import ora from 'ora'; 7 | import type { Argv, CommandModule } from 'yargs'; 8 | import { createServer, setApp } from '../lib/devServer'; 9 | import { ProjectTools } from '../lib/ProjectTools'; 10 | import { runShellCommand } from '../lib/shell'; 11 | import { F } from '../lib/StringFormatters'; 12 | import { logger } from '../utils/logger'; 13 | import { BuildApp } from './common/build'; 14 | 15 | export const aliases: string[] = []; 16 | export const desc: string = 'Enter development mode'; 17 | 18 | export const builder = (yargs: Argv) => 19 | yargs.options({ 20 | 'ignore-watcher-output': { 21 | desc: 'Ignores the watchers output (tsc)', 22 | alias: 'iwo', 23 | default: false, 24 | type: 'boolean', 25 | }, 26 | tunnel: { 27 | desc: 'Uses ngrok to tunnel the dev server', 28 | alias: 't', 29 | default: true, 30 | type: 'boolean', 31 | }, 32 | }); 33 | 34 | export const DevCommand: CommandModule<{}, { 'ignore-watcher-output': boolean; tunnel: boolean }> = { 35 | aliases, 36 | builder, 37 | command: 'dev', 38 | async handler(opts) { 39 | const devServerPort = 5002; 40 | 41 | const { 42 | root, 43 | prebuild, 44 | watcher: devScript, 45 | } = await ProjectTools.resolveProject({ 46 | cwd: process.cwd(), 47 | }); 48 | 49 | if (prebuild) { 50 | await runShellCommand(prebuild); 51 | } 52 | 53 | if (devScript) { 54 | runShellCommand(devScript, opts.ignoreWatcherOutput ? 'ignore' : 'inherit'); 55 | } 56 | 57 | const { clientId, publicKey, token } = await ProjectTools.resolveEnvironment(); 58 | 59 | const watcher = chokidar.watch(root); 60 | let timeout: NodeJS.Timeout | null = null; 61 | 62 | const devAction = async () => { 63 | const spinner = ora('Found change! Building project').start(); 64 | 65 | try { 66 | const entry = await BuildApp({ 67 | skipPrebuild: true, 68 | overrideTarget: { type: 'devServer' }, 69 | entryFileName: `entry-${Math.random().toString(36).substring(7)}.mjs`, 70 | }); 71 | 72 | const app = await import(await resolve(path.join(process.cwd(), entry), import.meta.url)); 73 | 74 | setApp(app.default, { 75 | clientId, 76 | publicKey, 77 | token, 78 | }); 79 | 80 | spinner.succeed(); 81 | } catch (e) { 82 | spinner.fail(String(e)); 83 | } 84 | }; 85 | 86 | logger.warn( 87 | [ 88 | "If you're using a prebuild script, it will not be run!", 89 | "Disploy's development mode expects the root of your project to be ready to run JavaScript.", 90 | "For example, if you're using typescript, you should run `tsc -w` alongside disploy's dev command.", 91 | ].join('\n'), 92 | ); 93 | 94 | devAction(); 95 | 96 | watcher.on('change', () => { 97 | if (timeout) clearTimeout(timeout); 98 | 99 | timeout = setTimeout(() => { 100 | devAction(); 101 | }, 1000); 102 | }); 103 | 104 | createServer(devServerPort); 105 | 106 | let url = `http://localhost:${devServerPort}`; 107 | 108 | if (opts.tunnel) { 109 | const spinner = ora('Tunneling to ngrok').start(); 110 | 111 | const tunnelUrl = await ngrok.connect({ 112 | addr: devServerPort, 113 | proto: 'http', 114 | }); 115 | 116 | url = tunnelUrl; 117 | 118 | spinner.succeed('Connected to ngrok'); 119 | } 120 | 121 | logger.info( 122 | [ 123 | 'Your bot is ready to receive interactions!', 124 | `1. Visit ${color.cyan(F.createAppSettingsURL(clientId))}`, 125 | `2. Set ${color.gray('INTERACTIONS ENDPOINT URL')} to ${color.cyan(F.createInteractionsURI(url))}`, 126 | ].join('\n'), 127 | ); 128 | ``; 129 | }, 130 | }; 131 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/commands/sync.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'import-meta-resolve'; 2 | import inquirer from 'inquirer'; 3 | import path from 'node:path'; 4 | import ora from 'ora'; 5 | import type { Argv, CommandModule } from 'yargs'; 6 | import { ProjectTools } from '../lib/ProjectTools'; 7 | import type { DisployStandaloneBundle } from '../types'; 8 | import { BuildApp } from './common/build'; 9 | 10 | export const aliases: string[] = []; 11 | export const desc: string = 'Deploy a project'; 12 | 13 | export const builder = (yargs: Argv) => yargs.options({}); 14 | 15 | export const SyncCommand: CommandModule = { 16 | aliases, 17 | builder, 18 | command: 'sync', 19 | async handler() { 20 | const { clientId, publicKey, token } = await ProjectTools.resolveEnvironment(); 21 | 22 | const input = await inquirer.prompt([ 23 | { 24 | type: 'list', 25 | name: 'syncStrategy', 26 | message: 'What sync strategy would you like to use?', 27 | choices: [ 28 | { 29 | name: 'Replace all existing commands', 30 | value: 'replace', 31 | }, 32 | { 33 | name: 'Merge with existing commands', 34 | value: 'merge', 35 | }, 36 | ], 37 | }, 38 | ]); 39 | 40 | const entry = await BuildApp({ 41 | skipPrebuild: true, 42 | overrideTarget: { type: 'standalone' }, 43 | entryFileName: `entry.mjs`, 44 | }); 45 | 46 | const { app, commands }: DisployStandaloneBundle = ( 47 | await import(await resolve(path.join(process.cwd(), entry), import.meta.url)) 48 | ).default; 49 | 50 | app.start({ 51 | clientId: clientId, 52 | publicKey: publicKey, 53 | token: token, 54 | }); 55 | 56 | const spinner = ora(`Syncing ${commands.length} commands`).start(); 57 | 58 | await app.commands.syncCommands(input.syncStrategy === 'replace' ? false : true); 59 | 60 | spinner.succeed(); 61 | }, 62 | }; 63 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/commands/test-server.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'import-meta-resolve'; 2 | import path from 'node:path'; 3 | import ora from 'ora'; 4 | import type { Argv, CommandModule } from 'yargs'; 5 | import { createServer, setApp } from '../lib/devServer'; 6 | import { ProjectTools } from '../lib/ProjectTools'; 7 | import { logger } from '../utils/logger'; 8 | import { BuildApp } from './common/build'; 9 | 10 | export const aliases: string[] = []; 11 | export const desc: string = 'Serve test-server'; 12 | 13 | export const builder = (yargs: Argv) => yargs.options({}); 14 | 15 | export const TestServerCommand: CommandModule = { 16 | aliases, 17 | builder, 18 | command: 'test-server', 19 | async handler() { 20 | const devServerPort = 5002; 21 | 22 | logger.warn( 23 | [ 24 | "If you're using a prebuild script, it will not be run!", 25 | "Disploy's development mode expects the root of your project to be ready to run JavaScript.", 26 | "For example, if you're using typescript, you should run `tsc -w` alongside disploy's dev command.", 27 | ].join('\n'), 28 | ); 29 | 30 | const { clientId, token } = await ProjectTools.resolveEnvironment(false); 31 | 32 | const spinner = ora('Found change! Building project').start(); 33 | const entry = await BuildApp({ 34 | skipPrebuild: true, 35 | overrideTarget: { type: 'devServer' }, 36 | entryFileName: `entry-${Math.random().toString(36).substring(7)}.mjs`, 37 | }); 38 | 39 | const app = await import(await resolve(path.join(process.cwd(), entry), import.meta.url)); 40 | 41 | setApp( 42 | app.default, 43 | { 44 | clientId: clientId, 45 | publicKey: null, 46 | token: token, 47 | }, 48 | true, 49 | ); 50 | 51 | logger.info(`Server Ready!`); 52 | logger.info(`URI: http://localhost:${devServerPort}/interactions`); 53 | 54 | spinner.succeed(); 55 | 56 | createServer(devServerPort); 57 | }, 58 | }; 59 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/disploy.ts: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | import dotenv from 'dotenv'; 4 | import path from 'node:path'; 5 | import yargs, { CommandModule } from 'yargs'; 6 | import { hideBin } from 'yargs/helpers'; 7 | import { BuildCommand } from './commands/build'; 8 | import { DeployCommand } from './commands/deploy'; 9 | import { DevCommand } from './commands/dev'; 10 | import { SyncCommand } from './commands/sync'; 11 | import { TestServerCommand } from './commands/test-server'; 12 | 13 | const cleanExit = function () { 14 | process.exit(); 15 | }; 16 | process.on('SIGINT', cleanExit); 17 | process.on('SIGTERM', cleanExit); 18 | 19 | (async () => { 20 | const commands = [SyncCommand, DevCommand, BuildCommand, DeployCommand, TestServerCommand]; 21 | 22 | const handler = yargs(hideBin(process.argv)); 23 | 24 | const globalOptions = await handler.options({ 25 | envFile: { 26 | type: 'string', 27 | alias: 'e', 28 | description: 'Path to the .env file to use', 29 | default: path.join(process.cwd(), '.env'), 30 | }, 31 | }).argv; 32 | 33 | dotenv.config({ path: globalOptions.envFile }); 34 | 35 | commands.forEach((command) => { 36 | handler.command(command as CommandModule); 37 | }); 38 | 39 | handler.demandCommand(1).parse(); 40 | })(); 41 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/lib/EnvTools.ts: -------------------------------------------------------------------------------- 1 | import { UserError } from './UserError'; 2 | 3 | export class EnvGetError extends Error { 4 | constructor(public key: string) { 5 | super(`Environment variable ${key} is not set`); 6 | this.name = 'EnvGetError'; 7 | } 8 | } 9 | 10 | export async function getEnvVar(key: string, required = true) { 11 | const value = process.env[key]; 12 | if (!value && required) { 13 | throw new EnvGetError(key); 14 | } 15 | return value || null; 16 | } 17 | 18 | // TODO: type arguments instead of returning String | null 19 | export async function batchGetEnvVars( 20 | keys: { 21 | key: string; 22 | required?: boolean; 23 | }[], 24 | ): Promise> { 25 | const errors: EnvGetError[] = []; 26 | const values = await Promise.all( 27 | keys.map(async ({ key, required = true }) => { 28 | try { 29 | return await getEnvVar(key, required); 30 | } catch (err: any) { 31 | if (typeof err === 'object' && err['name'] === 'EnvGetError') { 32 | errors.push(err as EnvGetError); 33 | } 34 | return null; 35 | } 36 | }), 37 | ); 38 | if (errors.length) { 39 | throw new UserError(`The following environment variables are not set: ${errors.map((err) => err.key).join(', ')}`); 40 | } 41 | 42 | return keys.reduce((acc, { key }, i) => { 43 | acc[key] = values[i] || null; 44 | return acc; 45 | }, {} as Record); 46 | } 47 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/lib/ProjectTools.ts: -------------------------------------------------------------------------------- 1 | import { DisployConfig, readConfig } from './disployConf'; 2 | import { batchGetEnvVars } from './EnvTools'; 3 | let config: DisployConfig | undefined; 4 | 5 | async function resolveProject({ cwd }: { cwd: string }) { 6 | if (!config) { 7 | const pkg = await readConfig(cwd); 8 | config = pkg; 9 | } 10 | 11 | return config; 12 | } 13 | 14 | async function resolveEnvironment(publicKeyRequired = true): Promise<{ 15 | token: string; 16 | publicKey: string | null; 17 | clientId: string; 18 | }> { 19 | const { DISCORD_TOKEN, DISCORD_PUBLIC_KEY, DISCORD_CLIENT_ID } = await batchGetEnvVars([ 20 | { 21 | key: 'DISCORD_TOKEN', 22 | }, 23 | { 24 | key: 'DISCORD_PUBLIC_KEY', 25 | required: publicKeyRequired, 26 | }, 27 | { 28 | key: 'DISCORD_CLIENT_ID', 29 | }, 30 | ]); 31 | 32 | return { 33 | token: DISCORD_TOKEN!, 34 | publicKey: DISCORD_PUBLIC_KEY, 35 | clientId: DISCORD_CLIENT_ID!, 36 | }; 37 | } 38 | 39 | export const ProjectTools = { 40 | resolveProject, 41 | resolveEnvironment, 42 | }; 43 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/lib/StringFormatters.ts: -------------------------------------------------------------------------------- 1 | const createAppSettingsURL = (appId: string) => `https://discord.com/developers/applications/${appId}/information`; 2 | 3 | const createInteractionsURI = (url: string) => `${url}/interactions`; 4 | 5 | export const StringFormatters = { createAppSettingsURL, createInteractionsURI }; 6 | 7 | export const F = StringFormatters; 8 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/lib/UserError.ts: -------------------------------------------------------------------------------- 1 | import { logger } from '../utils/logger'; 2 | 3 | export class UserError extends Error { 4 | constructor(message: string, killProcess: boolean = true) { 5 | super(message); 6 | 7 | logger.error(`⚠️ ${message}`); 8 | 9 | if (killProcess) { 10 | process.exit(1); 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/lib/WranglerWrapper.ts: -------------------------------------------------------------------------------- 1 | import { execSync } from 'child_process'; 2 | import type { DisployConfig } from './disployConf'; 3 | import { UserError } from './UserError'; 4 | 5 | export class WranglerWrapper { 6 | constructor(private readonly config: DisployConfig, private readonly entryFilePath: string) { 7 | try { 8 | execSync('wrangler --version', { stdio: 'ignore' }); 9 | } catch (e) { 10 | throw new UserError('Wrangler is not installed. Please install it with `npm install -g wrangler`.'); 11 | } 12 | } 13 | 14 | public async publish() { 15 | if (this.config.target.type !== 'cloudflare') { 16 | throw new UserError('This project is not configured to be deployed to Cloudflare Workers.'); 17 | } 18 | 19 | execSync( 20 | `wrangler publish ${this.entryFilePath} --compatibility-date 2022-11-05 --name ${this.config.target.name} --node-compat true`, 21 | { stdio: 'inherit' }, 22 | ); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/lib/compiler/assets/index.ts: -------------------------------------------------------------------------------- 1 | import { readFileSync } from 'node:fs'; 2 | import { join } from 'node:path'; 3 | 4 | export interface CodeGenAssets { 5 | [key: string]: (matchers?: Record) => string; 6 | } 7 | 8 | const cache = new Map(); 9 | 10 | export const CompilerAssets = new Proxy( 11 | {}, 12 | { 13 | get: (_, name) => { 14 | const code = 15 | cache.get(name as string) ?? readFileSync(join(__dirname, 'assets', 'code', `${String(name)}.js`), 'utf-8'); 16 | 17 | return createReplacer(code); 18 | }, 19 | }, 20 | ) as CodeGenAssets; 21 | 22 | function createReplacer(code: string) { 23 | return function replacer(matchers?: Record): string { 24 | if (!matchers) { 25 | return code; 26 | } 27 | 28 | let result = code; 29 | 30 | for (const key in matchers) { 31 | result = result.replace(new RegExp(`{{${key}}}`, 'g'), matchers[key]); 32 | } 33 | 34 | return result; 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/lib/compiler/constants.ts: -------------------------------------------------------------------------------- 1 | export const TempDir = '.disploy'; 2 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/lib/compiler/copyDir.ts: -------------------------------------------------------------------------------- 1 | import { copyFileSync, existsSync, mkdirSync, readdirSync, statSync } from 'fs'; 2 | import { join } from 'path'; 3 | 4 | export function copyDir(src: string, dest: string) { 5 | if (existsSync(src)) { 6 | if (statSync(src).isDirectory()) { 7 | if (!existsSync(dest)) { 8 | mkdirSync(dest); 9 | } 10 | readdirSync(src).forEach((file) => { 11 | copyDir(join(src, file), join(dest, file)); 12 | }); 13 | } else { 14 | copyFileSync(src, dest); 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/lib/compiler/globExportBundle.ts: -------------------------------------------------------------------------------- 1 | import glob from 'glob'; 2 | import { readFile, writeFile } from 'node:fs/promises'; 3 | import path from 'node:path'; 4 | import { CompilerAssets } from './assets'; 5 | 6 | interface Module { 7 | name: string; 8 | code: string; 9 | exactPath: string; 10 | path: string; 11 | } 12 | 13 | function parseModuleName(name: string) { 14 | return `${name.replace(/[^a-zA-Z0-9_]/g, '_')}_${Math.random().toString(36).slice(2)}`; 15 | } 16 | 17 | export async function globExportBundle( 18 | workbench: string, 19 | { 20 | pathFromWorkbench, 21 | name: exportedName, 22 | }: { 23 | pathFromWorkbench: string; 24 | name: string; 25 | }, 26 | ) { 27 | const modulesDir = path.join(workbench, pathFromWorkbench); 28 | 29 | const moduleFiles = await new Promise((resolve, reject) => { 30 | glob(`${modulesDir.replaceAll('\\', '/')}/**/*.js`, async (err, files) => { 31 | if (err) { 32 | reject(err); 33 | } else { 34 | const modules = await Promise.all( 35 | files.map(async (file) => { 36 | const contents = await readFile(file, 'utf8'); 37 | const match = contents.match(/export default/); 38 | 39 | if (!match) return null; 40 | 41 | return file; 42 | }), 43 | ); 44 | 45 | // resolve(modules.filter((module) => module !== null) as string[]); 46 | resolve( 47 | Promise.all( 48 | modules 49 | .filter((module) => module !== null) 50 | .map(async (module) => ({ 51 | name: path.basename(parseModuleName(module as string), '.js'), 52 | code: await readFile(module as string, 'utf-8'), 53 | exactPath: module as string, 54 | path: path.relative(modulesDir, module as string), 55 | })), 56 | ), 57 | ); 58 | } 59 | }); 60 | }); 61 | 62 | for (const module of moduleFiles) { 63 | const code = module.code.replace(/export default/, `export const ${module.name} = `); 64 | await writeFile(module.exactPath, code); 65 | } 66 | 67 | const moduleArray = CompilerAssets.array({ 68 | name: exportedName, 69 | imports: moduleFiles 70 | .map((module) => `import {${module.name}} from "./${pathFromWorkbench}/${module.path}";`) 71 | .join('\n'), 72 | array: moduleFiles.map((module) => `${path.basename(module.name, '.js')}`).join(',\n'), 73 | }); 74 | 75 | await writeFile(path.join(workbench, `${exportedName}.js`), moduleArray); 76 | } 77 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/lib/compiler/index.ts: -------------------------------------------------------------------------------- 1 | import { mkdir, rm, writeFile } from 'fs/promises'; 2 | import path from 'node:path'; 3 | import { rollup } from 'rollup'; 4 | import esbuild from 'rollup-plugin-esbuild'; 5 | import { logger } from '../../utils/logger'; 6 | import type { DisployConfig } from '../disployConf'; 7 | import { UserError } from '../UserError'; 8 | import { CompilerAssets } from './assets'; 9 | import { TempDir } from './constants'; 10 | import { copyDir } from './copyDir'; 11 | import { globExportBundle } from './globExportBundle'; 12 | 13 | function parseTarget(target: DisployConfig['target']) { 14 | switch (target.type) { 15 | case 'cloudflare': 16 | return 'cfWorkerEntry'; 17 | case 'standalone': 18 | return 'standaloneEntry'; 19 | case 'devServer': 20 | return 'devServerEntry'; 21 | default: 22 | throw new UserError(`Unknown target: ${target}`); 23 | } 24 | } 25 | 26 | export async function Compile({ 27 | root, 28 | target, 29 | entryFileName, 30 | }: { 31 | root: string; 32 | target: DisployConfig['target']; 33 | entryFileName: string; 34 | }) { 35 | // Remove the temp folder if it exists 36 | await rm(TempDir, { recursive: true, force: true }); 37 | // Create the temp folder 38 | await mkdir(TempDir, { recursive: true }); 39 | 40 | // Create a workbench folder inside the temp folder 41 | const workbenchDir = path.join(TempDir, 'workbench'); 42 | await mkdir(workbenchDir, { recursive: true }); 43 | 44 | // Copy the workbench folder to the temp folder 45 | copyDir(root, workbenchDir); 46 | 47 | // Bundle commands 48 | await globExportBundle(workbenchDir, { 49 | pathFromWorkbench: 'commands', 50 | name: 'Commands', 51 | }); 52 | 53 | // Bundle message component handlers 54 | await globExportBundle(workbenchDir, { 55 | pathFromWorkbench: 'handlers', 56 | name: 'Handlers', 57 | }); 58 | 59 | // Parse the target 60 | const entry = parseTarget(target); 61 | // Get the entry file name 62 | const input = path.join(workbenchDir, 'entry.js'); 63 | // Write the entry file 64 | await writeFile(input, CompilerAssets[entry]()); 65 | 66 | // Create a rollup bundle 67 | const bundle = await rollup({ 68 | input: input, 69 | plugins: [ 70 | // @ts-ignore - Plugin types mismatch 71 | esbuild({ 72 | platform: 'neutral', 73 | treeShaking: true, 74 | }), 75 | ], 76 | external: ['disploy'], 77 | onwarn: (warning) => { 78 | if (warning.code === 'UNRESOLVED_IMPORT') return; 79 | logger.warn(warning.message); 80 | }, 81 | }); 82 | 83 | // Get the output file name 84 | const output = path.join(TempDir, entryFileName); 85 | // Write the output file 86 | await bundle.write({ 87 | file: output, 88 | format: 'es', 89 | }); 90 | 91 | // Remove the workbench folder 92 | await rm(workbenchDir, { recursive: true, force: true }); 93 | 94 | // Return the output file name 95 | return output; 96 | } 97 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/lib/devServer/index.ts: -------------------------------------------------------------------------------- 1 | import bodyParser from 'body-parser'; 2 | import type { App, TRequest } from 'disploy'; 3 | import express from 'express'; 4 | import type { DisployStandaloneBundle } from '../../types'; 5 | import { logger } from '../../utils/logger'; 6 | 7 | let app: App | null = null; 8 | const remoteCommands: string[] = []; 9 | 10 | const server = express(); 11 | server.use(bodyParser.json()); 12 | 13 | server.post('/interactions', async (req, res) => { 14 | if (!app) { 15 | return void res.status(500).send('App not ready'); 16 | } 17 | 18 | const tReq: TRequest = { 19 | body: req.body, 20 | headers: req.headers, 21 | _request: req, 22 | }; 23 | 24 | const payload = await app.router.entry(tReq); 25 | const { status, headers, body } = payload.serialized; 26 | 27 | return void res.status(status).set(headers).send(body); 28 | }); 29 | 30 | export function createServer(port: number) { 31 | server.listen(port); 32 | } 33 | 34 | export async function setApp( 35 | newApp: DisployStandaloneBundle, 36 | options: { clientId: string; publicKey: string | null; token: string }, 37 | skipSync = false, 38 | ) { 39 | const firstTime = !app; 40 | 41 | app = newApp.app; 42 | app.start({ 43 | clientId: options.clientId, 44 | publicKey: options.publicKey, 45 | token: options.token, 46 | }); 47 | 48 | if (skipSync) return; 49 | 50 | if (firstTime) { 51 | remoteCommands.push(...(await app.commands.getRegisteredCommands()).map((c) => c.name)); 52 | } 53 | 54 | let needsSync = false; 55 | 56 | for (const command of newApp.commands) { 57 | if (!remoteCommands.includes(command.name)) { 58 | needsSync = true; 59 | break; 60 | } 61 | } 62 | 63 | if (needsSync) { 64 | logger.info('Syncing commands...'); 65 | await app.commands.syncCommands(); 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/lib/disployConf/index.ts: -------------------------------------------------------------------------------- 1 | import { readFile } from 'fs/promises'; 2 | import path from 'path'; 3 | import { z } from 'zod'; 4 | import { UserError } from '../UserError'; 5 | 6 | const disployConfigSchema = z.object({ 7 | prebuild: z.string().optional(), 8 | watcher: z.string().optional(), 9 | root: z.string(), 10 | target: z.union([ 11 | z.object({ 12 | type: z.literal('cloudflare'), 13 | name: z.string(), 14 | }), 15 | z.object({ 16 | type: z.literal('standalone'), 17 | }), 18 | z.object({ 19 | type: z.literal('devServer'), 20 | }), 21 | ]), 22 | }); 23 | 24 | export type DisployConfig = z.infer; 25 | 26 | export async function readConfig(cwd: string): Promise { 27 | try { 28 | const file = await readFile(path.join(cwd, 'disploy.json'), 'utf-8'); 29 | const config = await disployConfigSchema.parseAsync(JSON.parse(file)); 30 | return config; 31 | } catch (e: any) { 32 | switch (e.code) { 33 | case 'ENOENT': 34 | throw new UserError('disploy.json not found'); 35 | default: 36 | throw new UserError(e.message); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/lib/shell.ts: -------------------------------------------------------------------------------- 1 | import { spawn, StdioOptions } from 'node:child_process'; 2 | 3 | export async function runShellCommand(c: string, stdioMode: StdioOptions = 'ignore') { 4 | await new Promise((resolve, reject) => { 5 | try { 6 | spawn(c.split(' ')[0]!, c.split(' ').slice(1), { 7 | cwd: process.cwd(), 8 | stdio: stdioMode, 9 | }).on('exit', (code) => { 10 | if (code === 0) { 11 | resolve(); 12 | } else { 13 | reject(); 14 | } 15 | }); 16 | } catch (error) { 17 | reject(error); 18 | } 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { App, Command } from 'disploy'; 2 | 3 | export interface DisployStandaloneBundle { 4 | app: App; 5 | commands: Command[]; 6 | } 7 | -------------------------------------------------------------------------------- /packages/disploy/cli/src/utils/logger.ts: -------------------------------------------------------------------------------- 1 | import * as color from 'colorette'; 2 | 3 | class Logger { 4 | public constructor(private readonly options: { debug: boolean }) {} 5 | 6 | private log(message: string, form: (text: string) => string, ...args: any[]) { 7 | console.log(form(message), ...args); 8 | } 9 | 10 | private box(message: string) { 11 | const lines = message.split('\n'); 12 | const longestLine = lines.reduce((a, b) => (a.length > b.length ? a : b)); 13 | const box = `┌${'─'.repeat(longestLine.length + 2)}┐ 14 | ${lines.map((line) => `│ ${line}${' '.repeat(longestLine.length - line.length)} │`).join('\n')} 15 | └${'─'.repeat(longestLine.length + 2)}┘`; 16 | 17 | return box; 18 | } 19 | 20 | public debug(message: string, ...args: any[]) { 21 | if (!this.options.debug) return; 22 | 23 | this.log(message, color.cyan, ...args); 24 | } 25 | 26 | public info(message: string, ...args: any[]) { 27 | this.log(message, color.magenta, ...args); 28 | } 29 | 30 | public warn(message: string, ...args: any[]) { 31 | this.log(this.box(message), color.yellowBright, ...args); 32 | } 33 | 34 | public error(message: string, ...args: any[]) { 35 | this.log(message, color.red, ...args); 36 | } 37 | } 38 | 39 | export const logger = new Logger({ 40 | // eslint-disable-next-line turbo/no-undeclared-env-vars 41 | debug: true, 42 | }); 43 | -------------------------------------------------------------------------------- /packages/disploy/cli/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "include": ["./src"], 4 | "compilerOptions": { 5 | "outDir": "./dist", 6 | "noUncheckedIndexedAccess": false 7 | }, 8 | "exclude": ["dist", "build", "node_modules"] 9 | } 10 | -------------------------------------------------------------------------------- /packages/disploy/cliff.toml: -------------------------------------------------------------------------------- 1 | [git] 2 | conventional_commits = true 3 | filter_unconventional = true 4 | split_commits = false 5 | commit_parsers = [ 6 | { message = "^feat(disploy)", group = "Features" }, 7 | { message = "^fix(disploy)", group = "Bug Fixes" }, 8 | { message = "^doc(disploy)", group = "Documentation" }, 9 | { message = "^perf(disploy)", group = "Performance" }, 10 | { message = "^refactor(disploy)", group = "Refactor" }, 11 | { message = "^style(disploy)", group = "Styling" }, 12 | { message = "^test(disploy)", group = "Testing" }, 13 | ] 14 | protect_breaking_commits = false 15 | filter_commits = false 16 | tag_pattern = "v[0-9]*" 17 | skip_tags = "v0.1.0-beta.1" 18 | ignore_tags = "" 19 | date_order = false 20 | sort_commits = "oldest" 21 | link_parsers = [ 22 | { pattern = "#(\\d+)", href = "https://github.com/Disploy/disploy/issues/$1" }, 23 | ] 24 | limit_commits = 42 25 | -------------------------------------------------------------------------------- /packages/disploy/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "disploy", 3 | "version": "0.3.0", 4 | "license": "Apache-2.0", 5 | "main": "./dist/index.js", 6 | "source": "./src/index.ts", 7 | "types": "./dist/index.d.ts", 8 | "bin": "./dist/cli/disploy.js", 9 | "type": "module", 10 | "files": [ 11 | "dist" 12 | ], 13 | "contributors": [ 14 | "Tristan Camejo ", 15 | "TenDRILLL ", 16 | "Suneetti Pirneni " 17 | ], 18 | "keywords": [ 19 | "api", 20 | "bot", 21 | "discord", 22 | "node", 23 | "typescript", 24 | "cf-worker" 25 | ], 26 | "repository": { 27 | "type": "git", 28 | "url": "https://github.com/Disploy/disploy.git" 29 | }, 30 | "homepage": "https://disploy.dev", 31 | "scripts": { 32 | "build": "node scripts/architect.mjs build", 33 | "test": "vitest run", 34 | "dev": "node scripts/architect.mjs build -w", 35 | "type-check": "tsc --noEmit && cd cli && tsc --noEmit", 36 | "lint": "TIMING=1 eslint src/**/*.ts* --fix", 37 | "release": "cliff-jumper" 38 | }, 39 | "devDependencies": { 40 | "@babel/core": "^7.20.7", 41 | "@favware/cliff-jumper": "^1.9.0", 42 | "@types/express": "^4.17.15", 43 | "@types/glob": "^8.0.0", 44 | "@types/inquirer": "^9.0.3", 45 | "@types/node": "^18.11.17", 46 | "@types/yargs": "^17.0.17", 47 | "eslint": "8.30.0", 48 | "eslint-config-custom": "workspace:^", 49 | "next": "^13.1.1", 50 | "tsconfig": "workspace:^", 51 | "tsup": "^6.5.0", 52 | "typescript": "^4.9.4", 53 | "vite": "^4.0.3", 54 | "vitest": "^0.26.2" 55 | }, 56 | "dependencies": { 57 | "@disploy/rest": "workspace:^", 58 | "chokidar": "^3.5.3", 59 | "colorette": "^2.0.19", 60 | "discord-api-types": "^0.37.24", 61 | "dotenv": "^16.0.3", 62 | "esbuild": "^0.16.10", 63 | "eventemitter3": "^5.0.0", 64 | "express": "^4.18.2", 65 | "glob": "^8.0.3", 66 | "import-meta-resolve": "^2.2.0", 67 | "inquirer": "^9.1.4", 68 | "ngrok": "^4.3.3", 69 | "ora": "^6.1.2", 70 | "rollup": "^3.8.1", 71 | "rollup-plugin-esbuild": "^5.0.0", 72 | "tweetnacl": "^1.0.3", 73 | "yargs": "^17.6.2", 74 | "zod": "^3.20.2" 75 | }, 76 | "publishConfig": { 77 | "access": "public" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/disploy/scripts/architect.mjs: -------------------------------------------------------------------------------- 1 | import { spawn } from 'node:child_process'; 2 | import yargs from 'yargs'; 3 | import { hideBin } from 'yargs/helpers'; 4 | import { copyAssets, startWatching } from './copyAssets.mjs'; 5 | 6 | async function run(script) { 7 | const [command, ...args] = script.split(' '); 8 | return new Promise((resolve, reject) => { 9 | spawn(command, args, { stdio: 'inherit' }).on('exit', (code) => { 10 | if (code === 0) { 11 | resolve(); 12 | } else { 13 | reject(); 14 | } 15 | }); 16 | }); 17 | } 18 | 19 | yargs(hideBin(process.argv)) 20 | .command( 21 | 'build', 22 | '🏗️ Architect', 23 | () => {}, 24 | async (argv) => { 25 | const { watch } = argv; 26 | 27 | copyAssets(); 28 | watch && startWatching(); 29 | 30 | try { 31 | await run(`yarn tsup${watch ? ' --watch' : ''}`); 32 | } catch (e) { 33 | process.exit(1); 34 | } 35 | }, 36 | ) 37 | .options('watch', { 38 | alias: 'w', 39 | type: 'boolean', 40 | description: '🔎 Watch for changes', 41 | }) 42 | .demandCommand(1) 43 | .parse(); 44 | -------------------------------------------------------------------------------- /packages/disploy/scripts/copyAssets.mjs: -------------------------------------------------------------------------------- 1 | import chokidar from 'chokidar'; 2 | import { copyFileSync, existsSync, mkdirSync, readdirSync, rmdirSync, statSync } from 'fs'; 3 | import { join } from 'path'; 4 | 5 | const copy = (src, dest) => { 6 | if (existsSync(src)) { 7 | if (statSync(src).isDirectory()) { 8 | if (!existsSync(dest)) { 9 | mkdirSync(dest, { recursive: true }); 10 | } 11 | readdirSync(src).forEach((file) => { 12 | console.log(`📁 Copying ${file} from ${src} to ${dest}`); 13 | copy(join(src, file), join(dest, file)); 14 | }); 15 | } else { 16 | copyFileSync(src, dest); 17 | } 18 | } 19 | }; 20 | 21 | /** 22 | * @type {import('chokidar').FSWatcher} 23 | */ 24 | let listener = null; 25 | 26 | export const copyAssets = () => { 27 | if (existsSync('./dist/cli/assets')) { 28 | rmdirSync('./dist/cli/assets', { recursive: true }); 29 | } 30 | copy('./cli/assets', './dist/cli/assets'); 31 | }; 32 | 33 | export function startWatching() { 34 | listener = chokidar.watch('./cli/assets'); 35 | 36 | listener.on('all', () => copyAssets()); 37 | } 38 | 39 | export function stopWatching() { 40 | if (!listener) throw new Error('No listener to stop'); 41 | listener.close(); 42 | } 43 | -------------------------------------------------------------------------------- /packages/disploy/src/adapters/IAdapter.ts: -------------------------------------------------------------------------------- 1 | import type { App } from '../client/App'; 2 | 3 | export type IAdapter = (app: App, server: T) => void; 4 | export type IReqResAdapter = (app: App, req: REQ, res: RES) => void; 5 | -------------------------------------------------------------------------------- /packages/disploy/src/adapters/expressAdapter.ts: -------------------------------------------------------------------------------- 1 | import type * as Express from 'express'; 2 | import type { TRequest } from '../http'; 3 | import type { IAdapter } from './IAdapter'; 4 | 5 | export const expressAdapter: IAdapter = (app, server) => { 6 | server.post('/interactions', async (req, res) => { 7 | const tReq: TRequest = { 8 | body: req.body, 9 | headers: req.headers, 10 | _request: req, 11 | }; 12 | 13 | const payload = await app.router.entry(tReq); 14 | const { status, headers, body } = payload.serialized; 15 | 16 | res.status(status).set(headers).send(body); 17 | }); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/disploy/src/adapters/index.ts: -------------------------------------------------------------------------------- 1 | export * from './expressAdapter'; 2 | export * from './IAdapter'; 3 | export * from './nextAdapter'; 4 | -------------------------------------------------------------------------------- /packages/disploy/src/adapters/nextAdapter.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next/types'; 2 | import type { App } from '../client'; 3 | import type { TRequest } from '../http'; 4 | 5 | export function createNextAdapter(app: App) { 6 | return async function (req: NextApiRequest, res: NextApiResponse) { 7 | if (req.method !== 'POST') { 8 | res.status(405); 9 | res.json({ error: 'Method not allowed' }); 10 | return; 11 | } 12 | 13 | const tReq: TRequest = { 14 | body: req.body, 15 | headers: req.headers, 16 | _request: req, 17 | }; 18 | 19 | const payload = await app.router.entry(tReq); 20 | const { status, headers, body } = payload.serialized; 21 | 22 | for (const [key, value] of Object.entries(headers)) { 23 | res.setHeader(key, value as string); 24 | } 25 | 26 | res.status(status).send(body); 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /packages/disploy/src/client/App.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable turbo/no-undeclared-env-vars */ 2 | import { OptionalRestConfig, RequiredRestConfig, Rest } from '@disploy/rest'; 3 | import { Routes } from 'discord-api-types/v10'; 4 | import { Command, CommandManager } from '../commands'; 5 | import { MessageComponentHandler, MessageComponentManager } from '../message-components'; 6 | import { Router } from '../router'; 7 | import { ChannelManager, Guild, MessageManager, StructureManager, User } from '../structs'; 8 | import { ToBeFetched } from '../structs/ToBeFetched'; 9 | import { Logger, LoggerOptions } from '../utils'; 10 | 11 | export interface AppOptions { 12 | logger?: LoggerOptions; 13 | commands?: Command[]; 14 | handlers?: MessageComponentHandler[]; 15 | rest?: Omit & OptionalRestConfig; 16 | env?: Record; 17 | } 18 | 19 | export interface StartAppOptions { 20 | publicKey: string | null; 21 | clientId: string; 22 | token: string; 23 | } 24 | 25 | const DefaultAppOptions: Required = { 26 | logger: { 27 | debug: false, 28 | }, 29 | commands: [], 30 | handlers: [], 31 | rest: {}, 32 | env: {}, 33 | }; 34 | 35 | export class App { 36 | // Options 37 | private initOptions: Required; 38 | public token: string = 'not-ready'; 39 | public clientId: string = 'not-ready'; 40 | public publicKey: string = 'not-ready'; 41 | public env: Map = new Map(); 42 | 43 | // Managers 44 | public logger: Logger; 45 | public router: Router; 46 | public rest!: Rest; 47 | 48 | public users: StructureManager; 49 | public guilds: StructureManager; 50 | public channels: ChannelManager; 51 | public messages: MessageManager; 52 | 53 | // Handlers 54 | public commands: CommandManager; 55 | public handlers: MessageComponentManager; 56 | 57 | // Misc 58 | public user!: ToBeFetched; 59 | 60 | public constructor(options?: AppOptions) { 61 | this.initOptions = { ...DefaultAppOptions, ...(options ?? {}) }; 62 | 63 | this.logger = new Logger(this.initOptions.logger); 64 | this.initOptions.env && this._populateEnv(this.initOptions.env); 65 | // token, clientId & publicKey are set in start() 66 | 67 | // Managers 68 | this.router = new Router(this); 69 | // Rest is initialized in start() 70 | 71 | this.users = new StructureManager(this, User, (id) => this.rest.get(Routes.user(id))); 72 | this.guilds = new StructureManager(this, Guild, (id) => this.rest.get(Routes.guild(id))); 73 | this.channels = new ChannelManager(this); 74 | this.messages = new MessageManager(this); 75 | 76 | // Handlers 77 | this.commands = new CommandManager(this, this.initOptions.commands); 78 | this.handlers = new MessageComponentManager(this, this.initOptions.handlers); 79 | } 80 | 81 | public start(options: StartAppOptions): void { 82 | this.token = options.token; 83 | this.clientId = options.clientId; 84 | this.publicKey = options.publicKey ?? 'not-ready'; 85 | 86 | // Managers 87 | this.rest = new Rest({ token: this.token, ...this.initOptions?.rest }); 88 | this.rest.on('debug', (message) => this.logger.debug(message)); 89 | 90 | // Misc 91 | this.user = new ToBeFetched(this, User, options.clientId, (id) => this.rest.get(Routes.user(id))); 92 | 93 | // Initialize 94 | this.router.start(); 95 | 96 | this.logger.debug('App initialized.', { 97 | publicKey: options.publicKey, 98 | token: options.token.replace(/^(.{5}).*$/, '$1**********'), 99 | clientID: options.clientId, 100 | }); 101 | } 102 | 103 | private _populateEnv(env: Record) { 104 | for (const [key, value] of Object.entries(env)) { 105 | this.env.set(key, value); 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /packages/disploy/src/client/index.ts: -------------------------------------------------------------------------------- 1 | export * from './App'; 2 | -------------------------------------------------------------------------------- /packages/disploy/src/commands/Command.ts: -------------------------------------------------------------------------------- 1 | import type { APIApplicationCommandOption, ApplicationCommandType } from 'discord-api-types/v10'; 2 | import type { ChatInputInteraction } from '../structs'; 3 | import type { CommandInteraction } from '../structs/CommandInteraction'; 4 | import type { MessageContextMenuInteraction } from '../structs/MessageContextMenuInteraction'; 5 | import type { UserContextMenuInteraction } from '../structs/UserContextMenuInteraction'; 6 | 7 | export interface ApplicationCommand { 8 | name: string; 9 | type?: ApplicationCommandType; 10 | run(interaction: CommandInteraction): void | Promise; 11 | } 12 | 13 | export interface ChatInputCommand extends ApplicationCommand { 14 | description: string; 15 | options?: APIApplicationCommandOption[]; 16 | type?: ApplicationCommandType.ChatInput; 17 | run(interaction: ChatInputInteraction): void | Promise; 18 | } 19 | 20 | export interface MessageContextMenuCommand extends ApplicationCommand { 21 | type: ApplicationCommandType.Message; 22 | run(interaction: MessageContextMenuInteraction): void | Promise; 23 | } 24 | 25 | export interface UserContextMenuCommand extends ApplicationCommand { 26 | type: ApplicationCommandType.User; 27 | run(interaction: UserContextMenuInteraction): void | Promise; 28 | } 29 | 30 | export type ContextMenuCommand = UserContextMenuCommand | MessageContextMenuCommand; 31 | 32 | export type Command = ChatInputCommand | ContextMenuCommand; 33 | -------------------------------------------------------------------------------- /packages/disploy/src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Command'; 2 | export * from './CommandManager'; 3 | -------------------------------------------------------------------------------- /packages/disploy/src/http/RequestorError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Throw this error to indicate that it was the requestor's fault. 3 | */ 4 | export class RequestorError extends Error { 5 | public status: number = 400; 6 | 7 | public constructor(message: string, status: number = 400) { 8 | super(message); 9 | 10 | this.status = status; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/disploy/src/http/TParams.ts: -------------------------------------------------------------------------------- 1 | export interface TParams { 2 | [key: string]: string; 3 | } 4 | -------------------------------------------------------------------------------- /packages/disploy/src/http/TRequest.ts: -------------------------------------------------------------------------------- 1 | export interface TRequest { 2 | body: any; 3 | headers: { [key: string]: string | string[] | undefined }; 4 | _request: any; 5 | randId?: string; 6 | } 7 | -------------------------------------------------------------------------------- /packages/disploy/src/http/TResponse.ts: -------------------------------------------------------------------------------- 1 | export class TResponse { 2 | public serialized!: { 3 | body: any; 4 | headers: Record; 5 | status: number; 6 | }; 7 | 8 | public constructor() { 9 | this.serialized = { 10 | body: null, 11 | headers: {}, 12 | status: 200, 13 | }; 14 | } 15 | 16 | public json(body: any) { 17 | this.serialized.body = body; 18 | return this; 19 | } 20 | 21 | public status(status: number) { 22 | this.serialized.status = status; 23 | return this; 24 | } 25 | 26 | public setHeader(key: string, value: string) { 27 | this.serialized.headers[key] = value; 28 | return this; 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /packages/disploy/src/http/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RequestorError'; 2 | export * from './TParams'; 3 | export * from './TRequest'; 4 | export * from './TResponse'; 5 | -------------------------------------------------------------------------------- /packages/disploy/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './adapters'; 2 | export * from './client'; 3 | export * from './commands'; 4 | export * from './http'; 5 | export * from './message-components'; 6 | export * from './router'; 7 | export * from './structs'; 8 | export { DiscordChannel } from './types'; 9 | export { RuntimeConstants, SnowflakeUtil } from './utils'; 10 | -------------------------------------------------------------------------------- /packages/disploy/src/message-components/MessageComponentHandler.ts: -------------------------------------------------------------------------------- 1 | import type { BaseInteraction } from '../structs'; 2 | import type { ButtonInteraction } from '../structs/ButtonInteraction'; 3 | 4 | export interface BaseMessageComponentHandler { 5 | customId: string; 6 | run(interaction: BaseInteraction): void | Promise; 7 | } 8 | 9 | export interface ButtonHandler extends BaseMessageComponentHandler { 10 | run(interaction: ButtonInteraction): void | Promise; 11 | } 12 | 13 | export type MessageComponentHandler = ButtonHandler; 14 | -------------------------------------------------------------------------------- /packages/disploy/src/message-components/MessageComponentManager.ts: -------------------------------------------------------------------------------- 1 | import type { App } from '../client'; 2 | import { MessageComponentRoute } from '../router'; 3 | import type { MessageComponentHandler } from './MessageComponentHandler'; 4 | 5 | export class MessageComponentManager { 6 | private readonly handlers = new Map(); 7 | 8 | public constructor(private app: App, baseHandlers: MessageComponentHandler[]) { 9 | for (const handler of baseHandlers) { 10 | this.registerHandler(handler); 11 | } 12 | } 13 | 14 | /** 15 | * Get the registered message component handlers for this manager 16 | * @returns Registered handlers in this manager 17 | */ 18 | public getHandlers() { 19 | return this.handlers; 20 | } 21 | 22 | public registerHandler(handler: MessageComponentHandler) { 23 | this.app.router.addRoute(new MessageComponentRoute(this.app, handler)); 24 | this.handlers.set(handler.customId, handler); 25 | 26 | this.app.logger.debug(`Registered message component handler ${handler.customId}!`); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/disploy/src/message-components/index.ts: -------------------------------------------------------------------------------- 1 | export * from './MessageComponentHandler'; 2 | export * from './MessageComponentManager'; 3 | -------------------------------------------------------------------------------- /packages/disploy/src/router/ApplicationCommandRoute.ts: -------------------------------------------------------------------------------- 1 | import { InteractionType } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import type { ApplicationCommand } from '../commands'; 4 | import type { CommandInteraction } from '../structs/CommandInteraction'; 5 | import { RequestorError } from '../utils'; 6 | import { BaseRoute } from './BaseRoute'; 7 | 8 | export class ApplicationCommandRoute extends BaseRoute { 9 | public name: string; 10 | 11 | public constructor(app: App, private command: ApplicationCommand) { 12 | super(app); 13 | this.type = InteractionType.ApplicationCommand; 14 | this.name = command.name; 15 | } 16 | 17 | public async chatInputRun(interaction: CommandInteraction) { 18 | if (!this.command.run) { 19 | throw new RequestorError('Command does not have a run method.'); 20 | } 21 | 22 | // The await is required here since the slashRun method *can* be async. 23 | // This ensures the route calls the finish event after the slashRun method is done. 24 | // This is important because serverless functions will end the process after the finish event is called. 25 | await this.command.run(interaction); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/disploy/src/router/BaseRoute.ts: -------------------------------------------------------------------------------- 1 | import type { InteractionType } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | 4 | export class BaseRoute { 5 | /** 6 | * The type of interaction this route is for. 7 | */ 8 | public type!: InteractionType; 9 | 10 | public constructor(protected app: App) { 11 | Object.defineProperty(this, 'app', { value: app }); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/disploy/src/router/MessageComponentRoute.ts: -------------------------------------------------------------------------------- 1 | import { InteractionType } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import type { MessageComponentHandler } from '../message-components'; 4 | import type { ButtonInteraction } from '../structs/ButtonInteraction'; 5 | import { RequestorError } from '../utils'; 6 | import { BaseRoute } from './BaseRoute'; 7 | 8 | export class MessageComponentRoute extends BaseRoute { 9 | public customId: string; 10 | 11 | public constructor(app: App, private handler: MessageComponentHandler) { 12 | super(app); 13 | this.type = InteractionType.MessageComponent; 14 | this.customId = handler.customId; 15 | } 16 | 17 | public async buttonRun(interaction: ButtonInteraction) { 18 | if (!this.handler.run) { 19 | throw new RequestorError('Message component handler does not have a run method.'); 20 | } 21 | 22 | // The await is required here since the slashRun method *can* be async. 23 | // This ensures the route calls the finish event after the slashRun method is done. 24 | // This is important because serverless functions will end the process after the finish event is called. 25 | await this.handler.run(interaction); 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/disploy/src/router/RouteParams.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { RouteParams } from './RouteParams'; 3 | 4 | describe('RouteParams', () => { 5 | test('Parse(test-:id, test-123) -> id = 123', () => { 6 | const template = 'test-:id'; 7 | const data = 'test-123'; 8 | const params = new RouteParams(null, template, data); 9 | 10 | expect(params.getParam('id')).toBe('123'); 11 | }); 12 | 13 | test('Parse(test-:id-:id2, test-123-456) -> id = 123, id2 = 456', () => { 14 | const template = 'test-:id-:id2'; 15 | const data = 'test-123-456'; 16 | const params = new RouteParams(null, template, data); 17 | 18 | expect(params.getParam('id')).toBe('123'); 19 | expect(params.getParam('id2')).toBe('456'); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /packages/disploy/src/router/RouteParams.ts: -------------------------------------------------------------------------------- 1 | import type { App } from '../client'; 2 | import type { User } from '../structs'; 3 | 4 | export class RouteParams { 5 | private params: Record = {}; 6 | 7 | public constructor(private app: App | null, template: string, data: string) { 8 | const templateParts = template.split('-'); 9 | const dataParts = data.split('-'); 10 | 11 | for (let i = 0; i < templateParts.length; i++) { 12 | const templatePart = templateParts[i]; 13 | const dataPart = dataParts[i]; 14 | 15 | if (templatePart?.startsWith(':')) { 16 | dataPart && (this.params[templatePart.slice(1)] = dataPart); 17 | } 18 | } 19 | } 20 | 21 | /** 22 | * Get a parameter from the route. 23 | * @param name The name of the parameter 24 | * @returns A string representation of the parameter 25 | */ 26 | public getParam(name: string): string { 27 | const param = this.params[name]; 28 | 29 | if (!param) { 30 | throw new Error(`Param ${name} not found`); 31 | } 32 | 33 | return param; 34 | } 35 | 36 | /** 37 | * Get a user from the route parameters. 38 | * @param name The name of the parameter 39 | * @returns A User structure if the parameter is a valid user ID 40 | * @throws If the User cannot be found or the parameter is not a valid user ID 41 | */ 42 | public async getUserParam(name: string): Promise { 43 | if (!this.app) { 44 | throw new Error('Cannot get user param without an app'); 45 | } 46 | 47 | const id = this.getParam(name); 48 | 49 | return this.app.users.fetch(id); 50 | } 51 | 52 | /** 53 | * Match a template against a data string. 54 | * @param template The template to match against 55 | * @param data The data to match 56 | * @returns Whether the data matches the template 57 | * @example 'ping-:id' vs 'ping-123' => true 58 | * 'ping-:id' vs 'ping-123-456' => false 59 | */ 60 | public static matchTemplate(template: string, data: string): boolean { 61 | // Split the template and data into parts (parts are separated by a hyphen) 62 | const templateParts = template.split('-'); 63 | const dataParts = data.split('-'); 64 | 65 | // If the template and data have different lengths, they can't match 66 | if (templateParts.length !== dataParts.length) { 67 | return false; 68 | } 69 | 70 | // Loop over the template placeholders and delete them from the templateParts array and dataParts array 71 | // If the template and data have different lengths, they can't match 72 | for (let i = 0; i < templateParts.length; i++) { 73 | if (templateParts[i]?.startsWith(':')) { 74 | templateParts.splice(i, 1); 75 | dataParts.splice(i, 1); 76 | } 77 | } 78 | 79 | // We're left with only the non-placeholder parts, we can compare them 80 | // If they're not equal, the template and data can't match 81 | if (templateParts.join('-') !== dataParts.join('-')) { 82 | return false; 83 | } 84 | 85 | // If we've made it this far, the template and data match 86 | return true; 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /packages/disploy/src/router/RouterEvents.ts: -------------------------------------------------------------------------------- 1 | export const RouterEvents = { 2 | /** 3 | * Emitted when a route run is finished. 4 | * @tip Serverless functions will end the process after this event is emitted. 5 | */ 6 | FinishedRun(randId: string) { 7 | return `${randId}-finish`; 8 | }, 9 | /** 10 | * Emitted when a route run should respond. 11 | * @tip Use this when you want to respond to a route run. 12 | */ 13 | Respond(interactionId: string) { 14 | return `${interactionId}-respond`; 15 | }, 16 | }; 17 | -------------------------------------------------------------------------------- /packages/disploy/src/router/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ApplicationCommandRoute'; 2 | export * from './MessageComponentRoute'; 3 | export * from './RouteParams'; 4 | export * from './Router'; 5 | export * from './RouterEvents'; 6 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/Attachment.ts: -------------------------------------------------------------------------------- 1 | import type { APIAttachment, Snowflake } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import { Base } from './Base'; 4 | 5 | export class Attachment extends Base { 6 | /** 7 | * Attachment id 8 | */ 9 | public id: Snowflake; 10 | 11 | /** 12 | * Name of file attached 13 | */ 14 | public fileName: string; 15 | 16 | /** 17 | * Description for the file 18 | */ 19 | public description?: string; 20 | 21 | /** 22 | * The attachment's media type 23 | * 24 | * See https://en.wikipedia.org/wiki/Media_type 25 | */ 26 | public contentType?: string; 27 | 28 | /** 29 | * Size of file in bytes 30 | */ 31 | public size: number; 32 | 33 | /** 34 | * Source url of file 35 | */ 36 | public url: string; 37 | 38 | /** 39 | * A proxied url of file 40 | */ 41 | public proxyUrl: string; 42 | 43 | /** 44 | * Height of file (if image) 45 | */ 46 | public height?: number | null; 47 | 48 | /** 49 | * Width of file (if image) 50 | */ 51 | public width?: number | null; 52 | 53 | /** 54 | * Whether this attachment is ephemeral 55 | */ 56 | public ephemeral?: boolean; 57 | 58 | public constructor(app: App, raw: APIAttachment) { 59 | super(app); 60 | this.id = raw.id; 61 | this.fileName = raw.filename; 62 | this.description = raw.description; 63 | this.contentType = raw.content_type; 64 | this.size = raw.size; 65 | this.url = raw.url; 66 | this.proxyUrl = raw.proxy_url; 67 | this.height = raw.height; 68 | this.width = raw.width; 69 | this.ephemeral = raw.ephemeral; 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/Base.ts: -------------------------------------------------------------------------------- 1 | import type { App } from '../client'; 2 | 3 | export class Base { 4 | public app!: App; 5 | 6 | public constructor(app: App) { 7 | this.app = app; 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/BaseChannel.ts: -------------------------------------------------------------------------------- 1 | import type { APIChannel, ChannelType } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import { SnowflakeUtil } from '../utils'; 4 | import { ChannelMethods } from './ChannelMethods'; 5 | 6 | export abstract class BaseChannel extends ChannelMethods { 7 | /** 8 | * Timestamp of when the channel was created. 9 | */ 10 | public createdTimestamp!: number; 11 | 12 | /** 13 | * The type of the channel. 14 | */ 15 | public abstract type: ChannelType; 16 | 17 | public constructor(app: App, raw: APIChannel) { 18 | super(app, raw); 19 | this.id = raw.id; 20 | this.createdTimestamp = SnowflakeUtil.toTimestamp(this.id); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/BaseInteraction.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APIInteraction, 3 | APIInteractionResponseCallbackData, 4 | InteractionResponseType, 5 | MessageFlags, 6 | RESTGetAPIWebhookWithTokenMessageResult, 7 | RESTPatchAPIWebhookWithTokenMessageJSONBody, 8 | RESTPatchAPIWebhookWithTokenMessageResult, 9 | RESTPostAPIWebhookWithTokenJSONBody, 10 | RESTPostAPIWebhookWithTokenWaitResult, 11 | Routes, 12 | Snowflake, 13 | } from 'discord-api-types/v10'; 14 | import type { App } from '../client'; 15 | import { RouterEvents } from '../router'; 16 | import { DiscordAPIUtils, SnowflakeUtil } from '../utils'; 17 | import { Base } from './Base'; 18 | import { Guild } from './Guild'; 19 | import { GuildMember } from './GuildMember'; 20 | import type { Message } from './Message'; 21 | import { ToBeFetched } from './ToBeFetched'; 22 | import type { User } from './User'; 23 | 24 | export class BaseInteraction extends Base { 25 | /** 26 | * The ID of the interaction. 27 | */ 28 | public id!: Snowflake; 29 | 30 | /** 31 | * Timestamp of when the interaction was created. 32 | */ 33 | public createdTimestamp!: number; 34 | 35 | /** 36 | * The token of the interaction. 37 | */ 38 | public token!: string; 39 | 40 | /** 41 | * The User that invoked the interaction. 42 | */ 43 | public user: User; 44 | 45 | /** 46 | * The GuildMember who invoked the interaction. 47 | */ 48 | public member!: GuildMember | null; 49 | 50 | /** 51 | * The guild of the interaction. 52 | */ 53 | public guild!: ToBeFetched | null; 54 | 55 | public constructor(app: App, raw: APIInteraction) { 56 | super(app); 57 | this.id = raw.id; 58 | this.token = raw.token; 59 | this.createdTimestamp = SnowflakeUtil.toTimestamp(this.id); 60 | this.user = DiscordAPIUtils.resolveUserFromInteraction(app, raw); 61 | this.member = raw.member ? new GuildMember(this.app, raw.member) : null; 62 | this.guild = raw.guild_id 63 | ? new ToBeFetched(this.app, Guild, raw.guild_id, (id) => app.rest.get(Routes.guild(id))) 64 | : null; 65 | } 66 | 67 | /** 68 | * Defers the reply to the interaction. 69 | * @param options The options to defer the reply with. 70 | */ 71 | public deferReply(options?: { ephemeral?: boolean; fetchReply?: true }): Promise; 72 | public deferReply({ ephemeral = false, fetchReply = false } = {}): Promise { 73 | this.app.router.emit(RouterEvents.Respond(this.id), { 74 | type: InteractionResponseType.DeferredChannelMessageWithSource, 75 | data: { 76 | flags: ephemeral ? MessageFlags.Ephemeral : undefined, 77 | }, 78 | }); 79 | 80 | if (fetchReply) { 81 | return this.fetchReply(); 82 | } 83 | 84 | return Promise.resolve(null); 85 | } 86 | 87 | /** 88 | * Send a reply to the interaction. 89 | * @param payload The payload to send the reply with. 90 | * @param fetchReply Whether to fetch the reply that was sent. 91 | */ 92 | public reply(payload: APIInteractionResponseCallbackData, fetchReply?: true): Promise; 93 | public reply(payload: APIInteractionResponseCallbackData, fetchReply = false): Promise { 94 | this.app.router.emit(RouterEvents.Respond(this.id), { 95 | type: InteractionResponseType.ChannelMessageWithSource, 96 | data: payload, 97 | }); 98 | 99 | if (fetchReply) { 100 | return this.fetchReply(); 101 | } 102 | 103 | return Promise.resolve(null); 104 | } 105 | 106 | /** 107 | * Send a followup message to the interaction. 108 | * @param payload The payload to send the followup message with. 109 | * @returns The sent message. 110 | */ 111 | public async followUp(payload: APIInteractionResponseCallbackData): Promise { 112 | const res = await this.app.rest.post( 113 | `${Routes.webhook(this.app.clientId, this.token)}?wait=true`, 114 | payload, 115 | ); 116 | 117 | return this.app.messages.constructMessage({ ...res, guild_id: this.guild?.id }); 118 | } 119 | 120 | /** 121 | * Edit the original reply that has been sent by the interaction. 122 | * @param payload The payload to edit the reply with. 123 | * @returns The edited message. 124 | */ 125 | public async editReply(payload: RESTPatchAPIWebhookWithTokenMessageJSONBody) { 126 | return this.app.messages.constructMessage({ 127 | ...(await this.app.rest.patch< 128 | RESTPatchAPIWebhookWithTokenMessageJSONBody, 129 | RESTPatchAPIWebhookWithTokenMessageResult 130 | >(Routes.webhookMessage(this.app.clientId, this.token), payload)), 131 | guild_id: this.guild?.id, 132 | }); 133 | } 134 | 135 | /** 136 | * Fetch the message reply that has been sent by the interaction. 137 | * @returns The message that was sent by the interaction. 138 | */ 139 | public async fetchReply(id?: Snowflake) { 140 | return this.app.messages.constructMessage({ 141 | ...(await this.app.rest.get( 142 | Routes.webhookMessage(this.app.clientId, this.token, id), 143 | )), 144 | guild_id: this.guild?.id, 145 | }); 146 | } 147 | } 148 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/ButtonInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { APIMessageComponentButtonInteraction } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import type { RouteParams } from '../router'; 4 | import { MessageComponentInteraction } from './MessageComponentInteraction'; 5 | 6 | export class ButtonInteraction extends MessageComponentInteraction { 7 | public constructor(app: App, raw: APIMessageComponentButtonInteraction, params?: RouteParams) { 8 | super(app, raw, params); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/ChannelMethods.ts: -------------------------------------------------------------------------------- 1 | import { Routes, Snowflake } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import { Base } from './Base'; 4 | 5 | export class ChannelMethods extends Base { 6 | /** 7 | * The ID of the channel. 8 | */ 9 | public id: Snowflake; 10 | 11 | public constructor(app: App, raw: { id: string }) { 12 | super(app); 13 | this.id = raw.id; 14 | } 15 | 16 | /** 17 | * Deletes the channel. 18 | */ 19 | public async delete(): Promise { 20 | await this.app.rest.delete(Routes.channel(this.id)); 21 | } 22 | 23 | /** 24 | * Returns a string that represents the Channel object as a mention. 25 | * @returns A string that represents the Channel object as a mention. 26 | * @example interaction.reply(`You chose ${interaction.channel}`); // => You chose #general 27 | */ 28 | public override toString() { 29 | return this.id; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/ChatInputInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { APIChatInputApplicationCommandInteraction } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import { ChatInputInteractionOptions } from './ChatInputInteractionOptions'; 4 | import { CommandInteraction } from './CommandInteraction'; 5 | 6 | export class ChatInputInteraction extends CommandInteraction { 7 | /** 8 | * The options of the interaction. 9 | */ 10 | public options: ChatInputInteractionOptions; 11 | 12 | public constructor(app: App, public raw: APIChatInputApplicationCommandInteraction) { 13 | super(app, raw); 14 | this.options = new ChatInputInteractionOptions(app, this); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/ChatInputInteractionOptions.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | APIApplicationCommandInteractionDataAttachmentOption, 3 | APIApplicationCommandInteractionDataBooleanOption, 4 | APIApplicationCommandInteractionDataChannelOption, 5 | APIApplicationCommandInteractionDataIntegerOption, 6 | APIApplicationCommandInteractionDataNumberOption, 7 | APIApplicationCommandInteractionDataOption, 8 | APIApplicationCommandInteractionDataStringOption, 9 | APIApplicationCommandInteractionDataUserOption, 10 | } from 'discord-api-types/v10'; 11 | import type { App } from '../client'; 12 | import type { Attachment } from './Attachment'; 13 | import { Base } from './Base'; 14 | import type { BaseChannel } from './BaseChannel'; 15 | import type { ChatInputInteraction } from './ChatInputInteraction'; 16 | import { ChatInputInteractionResolvedOptions } from './ChatInputInteractionResolvedOptions'; 17 | import type { PartialGuildMember } from './PartialGuildMember'; 18 | import type { User } from './User'; 19 | 20 | export class ChatInputInteractionOptions extends Base { 21 | private resolved: ChatInputInteractionResolvedOptions; 22 | 23 | public constructor(app: App, private interaction: ChatInputInteraction) { 24 | super(app); 25 | this.resolved = new ChatInputInteractionResolvedOptions(this.interaction); 26 | } 27 | 28 | private getValue(key: string, nullable: boolean) { 29 | const value = this.interaction.raw.data.options?.find((option) => option.name === key) as T | undefined; 30 | 31 | if (!value && !nullable) { 32 | throw new Error(`Option "${key}" not found.`); 33 | } 34 | 35 | return value; 36 | } 37 | 38 | public getString(key: string): string; 39 | public getString(key: string, nullable: false): string; 40 | public getString(key: string, nullable: boolean): string | undefined; 41 | public getString(key: string, nullable = false): string | undefined { 42 | return this.getValue(key, nullable)?.value; 43 | } 44 | 45 | public getNumber(key: string): number; 46 | public getNumber(key: string, nullable: false): number; 47 | public getNumber(key: string, nullable: boolean): number | undefined; 48 | public getNumber(key: string, nullable = false): number | undefined { 49 | return this.getValue(key, nullable)?.value; 50 | } 51 | 52 | public getInteger(key: string): number; 53 | public getInteger(key: string, nullable: false): number; 54 | public getInteger(key: string, nullable: boolean): number | undefined; 55 | public getInteger(key: string, nullable = false): number | undefined { 56 | return this.getValue(key, nullable)?.value; 57 | } 58 | 59 | public getBoolean(key: string): boolean; 60 | public getBoolean(key: string, nullable: false): boolean; 61 | public getBoolean(key: string, nullable: boolean): boolean | undefined; 62 | public getBoolean(key: string, nullable = false): boolean | undefined { 63 | return this.getValue(key, nullable)?.value; 64 | } 65 | 66 | public getUser(key: string): User; 67 | public getUser(key: string, nullable: false): User; 68 | public getUser(key: string, nullable: boolean): User | undefined; 69 | public getUser(key: string, nullable = false): User | undefined { 70 | return this.resolved.users.get( 71 | this.getValue(key, nullable)?.value!, 72 | )!; 73 | } 74 | 75 | public getMember(key: string): PartialGuildMember; 76 | public getMember(key: string, nullable: false): PartialGuildMember; 77 | public getMember(key: string, nullable: boolean): PartialGuildMember | undefined; 78 | public getMember(key: string, nullable = false): PartialGuildMember | undefined { 79 | return this.resolved.members.get( 80 | this.getValue(key, nullable)?.value!, 81 | )!; 82 | } 83 | 84 | public getChannel(key: string): BaseChannel; 85 | public getChannel(key: string, nullable: false): BaseChannel; 86 | public getChannel(key: string, nullable: boolean): BaseChannel | undefined; 87 | public getChannel(key: string, nullable = false): BaseChannel | undefined { 88 | return this.resolved.channels.get( 89 | this.getValue(key, nullable)?.value!, 90 | )!; 91 | } 92 | 93 | public getAttachment(key: string): Attachment; 94 | public getAttachment(key: string, nullable: false): Attachment; 95 | public getAttachment(key: string, nullable: boolean): Attachment | undefined; 96 | public getAttachment(key: string, nullable = false): Attachment | undefined { 97 | return this.resolved.attachments.get( 98 | this.getValue(key, nullable)?.value!, 99 | )!; 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/ChatInputInteractionResolvedOptions.ts: -------------------------------------------------------------------------------- 1 | import type { Snowflake } from 'discord-api-types/v10'; 2 | import { Attachment } from './Attachment'; 3 | import { Base } from './Base'; 4 | import type { BaseChannel } from './BaseChannel'; 5 | import type { ChatInputInteraction } from './ChatInputInteraction'; 6 | import { PartialChannel } from './PartialChannel'; 7 | import { PartialGuildMember } from './PartialGuildMember'; 8 | import { User } from './User'; 9 | 10 | export class ChatInputInteractionResolvedOptions extends Base { 11 | /** 12 | * The interaction the resolved options belong to. 13 | */ 14 | public interaction: ChatInputInteraction; 15 | 16 | /** 17 | * The resolved members. 18 | */ 19 | public members: Map; 20 | 21 | /** 22 | * The resolved users. 23 | */ 24 | public users: Map; 25 | 26 | /** 27 | * The resolved channels. 28 | */ 29 | public channels: Map; 30 | 31 | /** 32 | * The resolved attachments. 33 | */ 34 | public attachments: Map; 35 | 36 | constructor(interaction: ChatInputInteraction) { 37 | super(interaction.app); 38 | this.interaction = interaction; 39 | this.members = new Map(); 40 | this.users = new Map(); 41 | this.channels = new Map(); 42 | this.attachments = new Map(); 43 | 44 | if (!interaction.raw.data.resolved) return; 45 | 46 | const { users, members, channels, attachments } = interaction.raw.data.resolved; 47 | 48 | if (users) { 49 | for (const user in users) { 50 | this.users.set(user, new User(this.interaction.app, users[user]!)); 51 | } 52 | } 53 | 54 | if (channels) { 55 | for (const channel in channels) { 56 | this.channels.set(channel, new PartialChannel(this.interaction.app, channels[channel]!)); 57 | } 58 | } 59 | 60 | if (members) { 61 | for (const member in members) { 62 | if (!this.users.has(member)) { 63 | this.interaction.app.logger.debug(`An invalid member with the id ${member} was provided`); 64 | } 65 | 66 | this.members.set(member, new PartialGuildMember(this.interaction.app, members[member]!)); 67 | } 68 | } 69 | 70 | if (attachments) { 71 | for (const attachment in attachments) { 72 | this.attachments.set(attachment, new Attachment(this.interaction.app, attachments[attachment]!)); 73 | } 74 | } 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/CommandInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { APIApplicationCommandInteraction, Snowflake } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import { BaseInteraction } from './BaseInteraction'; 4 | 5 | export class CommandInteraction extends BaseInteraction { 6 | /** 7 | * The ID of the command. 8 | */ 9 | public commandId: Snowflake; 10 | 11 | /** 12 | * The name of the command. 13 | */ 14 | public commandName: string; 15 | 16 | public constructor(app: App, raw: APIApplicationCommandInteraction) { 17 | super(app, raw); 18 | this.commandId = raw.data.id; 19 | this.commandName = raw.data.name; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/ContextMenuCommand.ts: -------------------------------------------------------------------------------- 1 | import type { APIContextMenuInteraction, Snowflake } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import { CommandInteraction } from './CommandInteraction'; 4 | 5 | export class ContextMenuInteraction extends CommandInteraction { 6 | /** 7 | * The ID of the target. 8 | */ 9 | public targetId: Snowflake; 10 | 11 | public constructor(app: App, raw: APIContextMenuInteraction) { 12 | super(app, raw); 13 | this.targetId = raw.data.target_id; 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/Guild.ts: -------------------------------------------------------------------------------- 1 | import { APIGuild, Routes, Snowflake } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import { SnowflakeUtil } from '../utils'; 4 | import { Base } from './Base'; 5 | import { GuildBan } from './GuildBan'; 6 | import { GuildMember } from './GuildMember'; 7 | import { GuildVoiceChannel } from './GuildVoiceChannel'; 8 | import { ChannelManager, StructureManager } from './managers'; 9 | import { ToBeFetched } from './ToBeFetched'; 10 | 11 | export class Guild extends Base { 12 | /** 13 | * The ID of the guild. 14 | */ 15 | public id!: Snowflake; 16 | 17 | /** 18 | * The owner ID of the guild. 19 | */ 20 | public ownerId!: Snowflake; 21 | 22 | /** 23 | * The name of the guild. 24 | */ 25 | public name!: string; 26 | 27 | /** 28 | * The AFK channel of the guild. 29 | */ 30 | public afkChannel!: ToBeFetched | null; 31 | 32 | /** 33 | * The AFK channel's ID of the guild. 34 | */ 35 | public afkChannelId!: Snowflake | null; 36 | 37 | /** 38 | * The time in seconds a user has to be AFK before being moved to the AFK channel. 39 | */ 40 | public afkTimeout!: number; 41 | 42 | /** 43 | * The ID of the application that owns the guild. (if it is a bot application) 44 | */ 45 | public applicationId!: Snowflake | null; 46 | 47 | /** 48 | * The approximate number of members in the guild. 49 | * @warning You will need to use {@link Guild#fetch} to get this value. 50 | */ 51 | public approximateMemberCount?: number; 52 | 53 | /** 54 | * The approximate number of presences in the guild. 55 | * @warning You will need to use {@link Guild#fetch} to get this value. 56 | */ 57 | public approximatePresenceCount?: number; 58 | 59 | /** 60 | * The hash of the guild banner 61 | */ 62 | public banner!: string | null; 63 | 64 | /** 65 | * The ban manager for this guild. 66 | */ 67 | public bans!: StructureManager; 68 | 69 | /** 70 | * Shortcut to {@link App#channels} 71 | */ 72 | public channels!: ChannelManager; 73 | 74 | /** 75 | * Timestamp of when the channel was created. 76 | */ 77 | public createdTimestamp!: number; 78 | 79 | /** 80 | * The description of the guild (if it has one). 81 | */ 82 | public description!: string | null; 83 | 84 | /** 85 | * The member manager for this guild. 86 | */ 87 | public members!: StructureManager; 88 | 89 | /** 90 | * The NSFW level for this guild. 91 | */ 92 | public nsfwLevel!: number; 93 | 94 | public constructor(app: App, raw: APIGuild) { 95 | super(app); 96 | this.patch(raw); 97 | this.channels = new ChannelManager(app, this.id); 98 | } 99 | 100 | private patch(raw: APIGuild): this { 101 | this.id = raw.id; 102 | this.ownerId = raw.owner_id; 103 | this.name = raw.name; 104 | 105 | this.afkChannel = raw.afk_channel_id 106 | ? new ToBeFetched(this.app, GuildVoiceChannel, raw.afk_channel_id, (id) => this.app.rest.get(Routes.channel(id))) 107 | : null; 108 | this.afkChannelId = raw.afk_channel_id; 109 | this.afkTimeout = raw.afk_timeout; 110 | this.applicationId = raw.application_id; 111 | this.banner = raw.banner; 112 | this.bans = new StructureManager(this.app, GuildBan, async (id) => { 113 | return { 114 | guild: this, 115 | raw: await this.app.rest.get(Routes.guildBan(this.id, id)), 116 | }; 117 | }); 118 | this.createdTimestamp = SnowflakeUtil.toTimestamp(this.id); 119 | this.description = raw.description; 120 | this.members = new StructureManager(this.app, GuildMember, async (id) => 121 | this.app.rest.get(Routes.guildMember(this.id, id)), 122 | ); 123 | 124 | if ('approximate_member_count' in raw) { 125 | this.approximateMemberCount = raw.approximate_member_count; 126 | } 127 | 128 | if ('approximate_presence_count' in raw) { 129 | this.approximatePresenceCount = raw.approximate_presence_count; 130 | } 131 | 132 | this.nsfwLevel = raw.nsfw_level; 133 | 134 | return this; 135 | } 136 | 137 | public async fetch(): Promise { 138 | return this.patch(await this.app.rest.get(`${Routes.guild(this.id)}?with_counts=true`)); 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/GuildBan.ts: -------------------------------------------------------------------------------- 1 | import type { APIBan } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import { Base } from './Base'; 4 | import type { Guild } from './Guild'; 5 | import { User } from './User'; 6 | 7 | export class GuildBan extends Base { 8 | /** 9 | * The guild this ban belongs to. 10 | */ 11 | public guild!: Guild; 12 | 13 | /** 14 | * The user this ban belongs to. 15 | */ 16 | public user!: User; 17 | 18 | /** 19 | * The reason for this ban. 20 | */ 21 | public reason!: string | null; 22 | 23 | public constructor(app: App, { raw, guild }: { raw: APIBan; guild: Guild }) { 24 | super(app); 25 | this.guild = guild; 26 | this.user = new User(app, raw.user); 27 | this.reason = raw.reason; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/GuildMember.ts: -------------------------------------------------------------------------------- 1 | import type { APIGuildMember } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import { PartialGuildMember } from './PartialGuildMember'; 4 | import { User } from './User'; 5 | 6 | export class GuildMember extends PartialGuildMember { 7 | /** 8 | * The User object of the member. 9 | */ 10 | public user!: User; 11 | 12 | /** 13 | * Whether the user is deafened in voice channels 14 | */ 15 | public deaf!: boolean | null; 16 | 17 | /** 18 | * Whether the user is muted in voice channels 19 | */ 20 | public mute!: boolean | null; 21 | 22 | public constructor(app: App, raw: APIGuildMember) { 23 | super(app, raw); 24 | this.user = new User(this.app, raw.user!); 25 | this.deaf = raw.deaf ? Boolean(raw.deaf) : null; 26 | this.mute = raw.mute ? Boolean(raw.mute) : null; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/GuildTextChannel.ts: -------------------------------------------------------------------------------- 1 | import { 2 | APIGuildChannel, 3 | APIMessage, 4 | ChannelType, 5 | RESTPostAPIChannelMessageJSONBody, 6 | Routes, 7 | Snowflake, 8 | } from 'discord-api-types/v10'; 9 | import type { App } from '../client'; 10 | import { BaseChannel } from './BaseChannel'; 11 | 12 | export class GuildTextChannel extends BaseChannel { 13 | /** 14 | * The ID of the guild. 15 | */ 16 | public guildId!: Snowflake; 17 | 18 | /** 19 | * The name of the channel. 20 | */ 21 | public name!: string; 22 | 23 | /** 24 | * The type of the channel. 25 | */ 26 | public type: ChannelType.GuildText = ChannelType.GuildText; 27 | 28 | public constructor(app: App, raw: APIGuildChannel) { 29 | super(app, raw); 30 | this.guildId = raw.guild_id!; 31 | this.name = raw.name!; 32 | this.type = ChannelType.GuildText; 33 | } 34 | 35 | /** 36 | * Returns a string that represents the Channel object as a mention. 37 | * @returns A string that represents the Channel object as a mention. 38 | * @example interaction.reply(`You chose ${interaction.channel}`); // => You chose #general 39 | */ 40 | public override toString() { 41 | return `<#${this.id}>`; 42 | } 43 | 44 | /** 45 | * Sends a message to the channel. 46 | * @param payload Message payload 47 | * @returns Created message 48 | */ 49 | public async send(payload: RESTPostAPIChannelMessageJSONBody) { 50 | return this.app.messages.constructMessage({ 51 | ...(await this.app.rest.post(Routes.channelMessages(this.id), { 52 | ...payload, 53 | })), 54 | guild_id: this.guildId, 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/GuildVoiceChannel.ts: -------------------------------------------------------------------------------- 1 | import { APIGuildChannel, ChannelType, Snowflake } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import { BaseChannel } from './BaseChannel'; 4 | 5 | export class GuildVoiceChannel extends BaseChannel { 6 | /** 7 | * The ID of the guild. 8 | */ 9 | public guildId!: Snowflake; 10 | 11 | /** 12 | * The name of the channel. 13 | */ 14 | public name!: string; 15 | 16 | /** 17 | * The type of the channel. 18 | */ 19 | public type: ChannelType.GuildVoice = ChannelType.GuildVoice; 20 | 21 | public constructor(app: App, raw: APIGuildChannel) { 22 | super(app, raw); 23 | this.guildId = raw.guild_id!; 24 | this.name = raw.name!; 25 | this.type = ChannelType.GuildVoice; 26 | } 27 | 28 | /** 29 | * Returns a string that represents the Channel object as a mention. 30 | * @returns A string that represents the Channel object as a mention. 31 | * @example interaction.reply(`You chose ${interaction.channel}`); // => You chose 🔊OnlyFriends 32 | */ 33 | public override toString() { 34 | return `<#${this.id}>`; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/MessageComponentInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { APIMessageComponentInteraction } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import { RouteParams } from '../router'; 4 | import { BaseInteraction } from './BaseInteraction'; 5 | 6 | export class MessageComponentInteraction extends BaseInteraction { 7 | /** 8 | * The custom ID of the component. 9 | */ 10 | public customId!: string; 11 | 12 | /** 13 | * The parsed parameters of the interaction from the custom ID. 14 | */ 15 | public params!: RouteParams; 16 | 17 | public constructor(app: App, raw: APIMessageComponentInteraction, params?: RouteParams) { 18 | super(app, raw); 19 | this.customId = raw.data.custom_id; 20 | this.params = params || new RouteParams(this.app, '', ''); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/MessageContextMenuInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { APIMessageApplicationCommandInteraction } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import { ContextMenuInteraction } from './ContextMenuCommand'; 4 | import type { Message } from './Message'; 5 | 6 | export class MessageContextMenuInteraction extends ContextMenuInteraction { 7 | /** 8 | * The target message. 9 | */ 10 | public readonly targetMessage: Message; 11 | 12 | public constructor(app: App, raw: APIMessageApplicationCommandInteraction) { 13 | super(app, raw); 14 | 15 | const resolvedMessage = raw.data.resolved.messages[this.targetId]!; 16 | this.targetMessage = app.messages.constructMessage({ ...resolvedMessage, guild_id: this.guild?.id }); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/PartialChannel.ts: -------------------------------------------------------------------------------- 1 | import type { APIPartialChannel, ChannelType } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import type { DiscordChannel } from '../types'; 4 | import { SnowflakeUtil } from '../utils'; 5 | import { ChannelMethods } from './ChannelMethods'; 6 | 7 | export class PartialChannel extends ChannelMethods { 8 | /** 9 | * Timestamp of when the channel was created. 10 | */ 11 | public createdTimestamp: number; 12 | 13 | /** 14 | * The name of the channel. 15 | */ 16 | public name: string | null; 17 | 18 | /** 19 | * The type of the channel. 20 | */ 21 | public type: ChannelType; 22 | 23 | public constructor(app: App, raw: APIPartialChannel) { 24 | super(app, raw); 25 | this.id = raw.id; 26 | this.name = raw.name ?? null; 27 | this.type = raw.type; 28 | this.createdTimestamp = SnowflakeUtil.toTimestamp(this.id); 29 | } 30 | 31 | /** 32 | * Fetch the full channel. 33 | * @returns The full channel. 34 | */ 35 | public async fetch(): Promise { 36 | return this.app.channels.fetch(this.id); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/PartialGuildMember.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | APIGuildMember, 3 | APIInteractionDataResolvedGuildMember, 4 | GatewayGuildMemberRemoveDispatchData, 5 | Snowflake, 6 | } from 'discord-api-types/v10'; 7 | import type { App } from '../client'; 8 | import { Base } from './Base'; 9 | 10 | export class PartialGuildMember extends Base { 11 | /** 12 | * The nickname of the member. 13 | */ 14 | public nickname!: string | null; 15 | 16 | /** 17 | * The member's guild avatar hash. 18 | */ 19 | public avatar!: string | null; 20 | 21 | /** 22 | * Array of role object ids. 23 | */ 24 | public roles!: Snowflake[]; 25 | 26 | /** 27 | * When the user joined the guild. 28 | */ 29 | public joinedAt!: string; 30 | 31 | /** 32 | * When the user started boosting the guild. 33 | */ 34 | public premiumSince!: string | null; 35 | 36 | /** 37 | * Whether the user has not yet passed the guild's Membership Screening requirements. 38 | */ 39 | public pending!: boolean | null; 40 | 41 | /** 42 | * Timestamp of when the time out will be removed; until then, they cannot interact with the guild. 43 | */ 44 | public communicationDisabledUntil!: boolean | null; 45 | 46 | public constructor( 47 | app: App, 48 | raw: APIGuildMember | APIInteractionDataResolvedGuildMember | GatewayGuildMemberRemoveDispatchData, 49 | ) { 50 | super(app); 51 | this.nickname = 'nick' in raw ? raw.nick ?? null : null; 52 | this.avatar = 'avatar' in raw ? raw.avatar ?? null : null; 53 | this.roles = 'roles' in raw ? raw.roles : []; 54 | this.joinedAt = 'joined_at' in raw ? raw.joined_at : ''; 55 | this.premiumSince = 'premium_since' in raw ? raw.premium_since ?? null : null; 56 | this.pending = 'pending' in raw ? raw.pending ?? null : null; 57 | this.communicationDisabledUntil = 58 | 'communication_disabled_until' in raw ? Boolean(raw.communication_disabled_until) ?? null : null; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/ToBeFetched.ts: -------------------------------------------------------------------------------- 1 | import type { App } from '../client/App'; 2 | import type { NonRuntimeClass, StructureConstructor } from '../types'; 3 | import type { Base } from './Base'; 4 | 5 | export class ToBeFetched { 6 | public constructor( 7 | private app: App, 8 | private object: NonRuntimeClass, 9 | /** 10 | * The ID of the held object. 11 | */ 12 | public id: string, 13 | private _fetch: (id: string) => Promise, 14 | ) {} 15 | 16 | public async fetch(): Promise { 17 | const raw = await this._fetch(this.id); 18 | const Structure = this.object as unknown as StructureConstructor; 19 | 20 | return new Structure(this.app, raw); 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/User.ts: -------------------------------------------------------------------------------- 1 | import type { APIUser, Snowflake } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import { Base } from './Base'; 4 | 5 | export class User extends Base { 6 | /** 7 | * The ID of the user. 8 | * @example '97470053615673344' 9 | */ 10 | public id!: Snowflake; 11 | 12 | /** 13 | * The username of the user. 14 | * @example 'Disploy' 15 | */ 16 | public username!: string; 17 | 18 | /** 19 | * The tag of the user. 20 | * @example 'tristan#0005' 21 | */ 22 | public get tag(): `${string}#${string}` { 23 | return `${this.username}#${this.discriminator}`; 24 | } 25 | 26 | /** 27 | * The discriminator of the user. 28 | * @example '0005' 29 | */ 30 | public discriminator!: string; 31 | 32 | public constructor(app: App, raw: APIUser) { 33 | super(app); 34 | this.id = raw.id; 35 | this.username = raw.username; 36 | this.discriminator = raw.discriminator; 37 | } 38 | 39 | /** 40 | * Returns a string that represents the User object as a mention. 41 | * @returns A string that represents the User object as a mention. 42 | * @example interaction.reply(`Hey ${interaction.user}`); // => Hey @tristan 43 | */ 44 | public override toString() { 45 | return `<@${this.id}>`; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/UserContextMenuInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { APIUserApplicationCommandInteraction } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import { ContextMenuInteraction } from './ContextMenuCommand'; 4 | import { User } from './User'; 5 | 6 | export class UserContextMenuInteraction extends ContextMenuInteraction { 7 | /** 8 | * The target user. 9 | */ 10 | public readonly targetUser: User; 11 | 12 | public constructor(app: App, raw: APIUserApplicationCommandInteraction) { 13 | super(app, raw); 14 | 15 | const resolvedUser = raw.data.resolved.users[this.targetId]!; 16 | this.targetUser = new User(app, resolvedUser); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/UserInteraction.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | APIInteractionDataResolvedGuildMember, 3 | APIUserApplicationCommandInteraction, 4 | Snowflake, 5 | } from 'discord-api-types/v10'; 6 | import type { App } from '../client'; 7 | import { BaseInteraction } from './BaseInteraction'; 8 | import { PartialGuildMember } from './PartialGuildMember'; 9 | import { User } from './User'; 10 | 11 | export class UserInteraction extends BaseInteraction { 12 | /** 13 | * The ID of the command. 14 | */ 15 | public commandId!: Snowflake; 16 | 17 | /** 18 | * The name of the command. 19 | */ 20 | public commandName!: string; 21 | 22 | /** 23 | * The targeted User's id. 24 | */ 25 | public targetId!: Snowflake; 26 | 27 | /** 28 | * The targeted GuildMember. 29 | */ 30 | public targetMember!: PartialGuildMember | null; 31 | 32 | /** 33 | * The targeted User. 34 | */ 35 | public targetUser!: User | null; 36 | 37 | public constructor(app: App, public raw: APIUserApplicationCommandInteraction) { 38 | super(app, raw); 39 | this.commandId = raw.data.id; 40 | this.commandName = raw.data.name; 41 | this.targetId = raw.data.target_id; 42 | this.targetMember = raw.data.resolved.members 43 | ? new PartialGuildMember( 44 | this.app, 45 | raw.data.resolved.members[this.targetId] as APIInteractionDataResolvedGuildMember, 46 | ) 47 | : null; 48 | this.targetUser = new User(this.app, raw.data.resolved.users[raw.data.target_id]!); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/index.ts: -------------------------------------------------------------------------------- 1 | export * from './BaseChannel'; 2 | export * from './BaseInteraction'; 3 | export * from './ButtonInteraction'; 4 | export * from './ChatInputInteraction'; 5 | export * from './ChatInputInteractionOptions'; 6 | export * from './CommandInteraction'; 7 | export * from './ContextMenuCommand'; 8 | export * from './Guild'; 9 | export * from './GuildBan'; 10 | export * from './GuildMember'; 11 | export * from './GuildTextChannel'; 12 | export * from './managers'; 13 | export * from './Message'; 14 | export * from './MessageComponentInteraction'; 15 | export * from './MessageContextMenuInteraction'; 16 | export * from './PartialChannel'; 17 | export * from './PartialGuildMember'; 18 | export * from './User'; 19 | export * from './UserContextMenuInteraction'; 20 | export * from './UserInteraction'; 21 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/managers/ChannelManager.ts: -------------------------------------------------------------------------------- 1 | import { APIChannel, ChannelType, RESTGetAPIChannelResult, Routes } from 'discord-api-types/v10'; 2 | import type { App } from '../../client'; 3 | import type { DiscordChannel } from '../../types'; 4 | import { Base } from '../Base'; 5 | import { GuildTextChannel } from '../GuildTextChannel'; 6 | import { GuildVoiceChannel } from '../GuildVoiceChannel'; 7 | 8 | export class ChannelManager extends Base { 9 | private guildId?: string; 10 | 11 | /** 12 | * A manager for fetching channels. 13 | * @param app 14 | * @param guildId The ID of the guild to lock the manager to. 15 | */ 16 | public constructor(app: App, guildId?: string) { 17 | super(app); 18 | this.guildId = guildId; 19 | } 20 | 21 | /** 22 | * Fetch a channel by its ID. 23 | * @param id The ID of the channel to fetch. 24 | * @returns A constructed channel structure. 25 | */ 26 | public async fetch(id: string): Promise { 27 | const raw = await this.app.rest.get(Routes.channel(id)); 28 | 29 | return this.constructChannel(raw); 30 | } 31 | 32 | /** 33 | * Construct a channel from a raw channel object. 34 | * @param raw The raw channel data. 35 | * @returns A constructed channel structure. 36 | */ 37 | public constructChannel(raw: APIChannel): DiscordChannel { 38 | if (this.guildId !== undefined && 'guild_id' in raw && raw.guild_id !== this.guildId) { 39 | throw new Error(`Channel is not in the guild (${this.guildId})`); 40 | } 41 | 42 | switch (raw.type) { 43 | case ChannelType.GuildText: 44 | return new GuildTextChannel(this.app, raw); 45 | case ChannelType.GuildVoice: 46 | return new GuildVoiceChannel(this.app, raw); 47 | default: 48 | throw new Error(`Unknown channel type: ${raw.type}`); 49 | } 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/managers/MessageManager.ts: -------------------------------------------------------------------------------- 1 | import { APIMessage, RESTGetAPIChannelMessageResult, Routes } from 'discord-api-types/v10'; 2 | import type { App } from '../../client'; 3 | import { Base } from '../Base'; 4 | import { Message } from '../Message'; 5 | 6 | export class MessageManager extends Base { 7 | /** 8 | * A manager for manipulating messages. 9 | * @param app 10 | * @param guildId The ID of the guild to lock the manager to. 11 | */ 12 | public constructor(app: App) { 13 | super(app); 14 | } 15 | 16 | /** 17 | * Fetch a message by its channel & message ID. 18 | * @param gid The ID of the guild the message is in. 19 | * @param cid The ID the channel the message is in. 20 | * @param mid The ID of the message to fetch. 21 | * @returns A constructed channel structure. 22 | */ 23 | public async fetch(gid: string, cid: string, mid: string): Promise; 24 | /** 25 | * Fetch a message by its channel & message ID. 26 | * @param cid The ID the channel the message is in. 27 | * @param mid The ID of the message to fetch. 28 | * @returns A constructed channel structure. 29 | */ 30 | public async fetch(cid: string, mid: string): Promise; 31 | public async fetch(arg1: string, arg2: string, arg3?: string): Promise { 32 | const [gid, cid, mid] = arg3 ? [arg1, arg2, arg3] : [undefined, arg1, arg2]; 33 | const raw = await this.app.rest.get(Routes.channelMessage(cid, mid)); 34 | 35 | return this.constructMessage({ ...raw, guild_id: gid }); 36 | } 37 | 38 | /** 39 | * Construct a message from a raw message object. 40 | * @param raw The raw message data. 41 | * @returns A constructed message structure. 42 | */ 43 | public constructMessage(raw: APIMessage & { guild_id?: string }): Message { 44 | return new Message(this.app, raw); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/managers/StructureManager.test.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, test } from 'vitest'; 2 | import { Base } from '../Base'; 3 | import { StructureManager } from './StructureManager'; 4 | 5 | interface ExampleData { 6 | id: string; 7 | name: string; 8 | } 9 | 10 | async function fetchExampleData(id: string): Promise { 11 | return { 12 | id, 13 | name: `Example ${id}`, 14 | }; 15 | } 16 | 17 | class ExampleClass extends Base { 18 | public id: string; 19 | public name: string; 20 | 21 | public constructor(_: null, raw: ExampleData) { 22 | super(null!); 23 | this.id = raw.id; 24 | this.name = raw.name; 25 | } 26 | } 27 | 28 | const ExampleStructureManager = new StructureManager(null!, ExampleClass, (id) => fetchExampleData(id)); 29 | 30 | describe('StructureManager', () => { 31 | test('Example fetch', async () => { 32 | const tristan = await ExampleStructureManager.fetch('tristan'); 33 | 34 | expect(tristan).toBeInstanceOf(ExampleClass); 35 | expect(tristan.id).toBe('tristan'); 36 | expect(tristan.name).toBe('Example tristan'); 37 | }); 38 | }); 39 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/managers/StructureManager.ts: -------------------------------------------------------------------------------- 1 | import type { App } from '../../client'; 2 | import type { NonRuntimeClass, StructureConstructor } from '../../types'; 3 | import { Base } from '../Base'; 4 | 5 | export class StructureManager extends Base { 6 | public constructor(app: App, private object: NonRuntimeClass, private _fetch: (id: string) => Promise) { 7 | super(app); 8 | } 9 | 10 | public async fetch(id: string): Promise { 11 | const raw = await this._fetch(id); 12 | const Structure = this.object as unknown as StructureConstructor; 13 | 14 | return new Structure(this.app, raw); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/disploy/src/structs/managers/index.ts: -------------------------------------------------------------------------------- 1 | export * from './ChannelManager'; 2 | export * from './MessageManager'; 3 | export * from './StructureManager'; 4 | -------------------------------------------------------------------------------- /packages/disploy/src/types/DiscordChannel.ts: -------------------------------------------------------------------------------- 1 | import type { GuildTextChannel } from '../structs/GuildTextChannel'; 2 | import type { GuildVoiceChannel } from '../structs/GuildVoiceChannel'; 3 | 4 | export type DiscordChannel = GuildTextChannel | GuildVoiceChannel; 5 | -------------------------------------------------------------------------------- /packages/disploy/src/types/NonRuntimeClass.ts: -------------------------------------------------------------------------------- 1 | export type NonRuntimeClass = new (...args: any[]) => T; 2 | -------------------------------------------------------------------------------- /packages/disploy/src/types/StructureConstructor.ts: -------------------------------------------------------------------------------- 1 | import type { App } from '../client'; 2 | import type { Base } from '../structs/Base'; 3 | 4 | export type StructureConstructor = new (app: App, raw: unknown) => T; 5 | -------------------------------------------------------------------------------- /packages/disploy/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DiscordChannel'; 2 | export * from './NonRuntimeClass'; 3 | export * from './StructureConstructor'; 4 | -------------------------------------------------------------------------------- /packages/disploy/src/utils/DiscordAPIUtils.ts: -------------------------------------------------------------------------------- 1 | import type { APIInteraction } from 'discord-api-types/v10'; 2 | import type { App } from '../client'; 3 | import { User } from '../structs'; 4 | 5 | /** 6 | * Resolves a user from an interaction from `.user` or `.member.user` 7 | * @param raw The raw interaction data. 8 | * @returns The user structure from Disploy, if it exists. 9 | */ 10 | function resolveUserFromInteraction(app: App, raw: APIInteraction): User { 11 | let attemptedUser: User | null; 12 | 13 | if (raw.member) { 14 | attemptedUser = raw.member.user ? new User(app, raw.member.user) : null; 15 | } else { 16 | attemptedUser = raw.user ? new User(app, raw.user) : null; 17 | } 18 | 19 | if (!attemptedUser) { 20 | throw new Error('Could not resolve user from interaction.'); 21 | } 22 | 23 | return attemptedUser; 24 | } 25 | 26 | export const DiscordAPIUtils = { 27 | resolveUserFromInteraction, 28 | }; 29 | -------------------------------------------------------------------------------- /packages/disploy/src/utils/Logger.ts: -------------------------------------------------------------------------------- 1 | export interface LoggerOptions { 2 | /** 3 | * Whether to log debug messages. 4 | */ 5 | debug: boolean; 6 | } 7 | 8 | export class Logger { 9 | public constructor(private readonly options: LoggerOptions) {} 10 | 11 | private log(message: string, ...args: any[]) { 12 | console.log(message, ...args); 13 | } 14 | 15 | public debug(message: string, ...args: any[]) { 16 | if (!this.options.debug) return; 17 | 18 | this.log(message, ...args); 19 | } 20 | 21 | public info(message: string, ...args: any[]) { 22 | this.log(message, ...args); 23 | } 24 | 25 | public warn(message: string, ...args: any[]) { 26 | this.log(message, ...args); 27 | } 28 | 29 | public error(message: string, ...args: any[]) { 30 | this.log(message, ...args); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /packages/disploy/src/utils/RequestorError.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Throw this error to indicate that it was the requestor's fault. 3 | */ 4 | export class RequestorError extends Error { 5 | public status: number = 400; 6 | 7 | public constructor(message: string, status: number = 400) { 8 | super(message); 9 | 10 | this.status = status; 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /packages/disploy/src/utils/SnowflakeUtil.ts: -------------------------------------------------------------------------------- 1 | export class SnowflakeUtil { 2 | /** 3 | * Converts a snowflake to a binary string. 4 | * @param sf Snowflake 5 | * @returns Binary representation of the snowflake 6 | * @credit https://github.com/discordjs/discord.js/blob/5ec04e077bbbb9799f3ef135cade84b77346ef20/src/util/SnowflakeUtil.js#62 7 | */ 8 | public static toBinary(sf: string): string { 9 | let bin = ''; 10 | let high = parseInt(sf.slice(0, -10)) || 0; 11 | let low = parseInt(sf.slice(-10)); 12 | while (low > 0 || high > 0) { 13 | bin = String(low & 1) + bin; 14 | low = Math.floor(low / 2); 15 | if (high > 0) { 16 | low += 5_000_000_000 * (high % 2); 17 | high = Math.floor(high / 2); 18 | } 19 | } 20 | return bin; 21 | } 22 | 23 | /** 24 | * Converts a snowflake to a timestamp. 25 | * @param sf Snowflake 26 | * @returns Timestamp of the snowflake 27 | */ 28 | public static toTimestamp(sf: string): number { 29 | const BINARY = this.toBinary(sf).toString().padStart(64, '0'); 30 | 31 | return parseInt(BINARY.substring(0, 42), 2) + 1_420_070_400_000; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /packages/disploy/src/utils/Verify.ts: -------------------------------------------------------------------------------- 1 | export class Verify { 2 | public constructor(public publicKey: string) {} 3 | 4 | async verify( 5 | // @ts-expect-error Not used var 6 | body: string, 7 | // @ts-expect-error Not used var 8 | signature: string, 9 | // @ts-expect-error Not used var 10 | timestamp: string, 11 | ): Promise { 12 | throw new Error('Not implemented'); 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /packages/disploy/src/utils/VerifyCFW.ts: -------------------------------------------------------------------------------- 1 | import { Verify } from './Verify'; 2 | 3 | export class VerifyCFW extends Verify { 4 | private encoder = new TextEncoder(); 5 | 6 | private PublicKey = crypto.subtle.importKey( 7 | 'raw', 8 | this.hex2bin(this.publicKey), 9 | { 10 | name: 'NODE-ED25519', 11 | namedCurve: 'NODE-ED25519', 12 | // @ts-expect-error Cloudflare worker runtime 13 | public: true, 14 | }, 15 | true, 16 | ['verify'], 17 | ); 18 | 19 | private hex2bin(hex: string) { 20 | const buf = new Uint8Array(Math.ceil(hex.length / 2)); 21 | for (var i = 0; i < buf.length; i++) { 22 | buf[i] = parseInt(hex.substr(i * 2, 2), 16); 23 | } 24 | return buf; 25 | } 26 | 27 | override async verify(body: string, signature: string, timestamp: string) { 28 | const verified = await crypto.subtle.verify( 29 | 'NODE-ED25519', 30 | await this.PublicKey, 31 | this.hex2bin(signature), 32 | this.encoder.encode(timestamp + body), 33 | ); 34 | if (!verified) return false; 35 | 36 | return true; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /packages/disploy/src/utils/VerifyNode.ts: -------------------------------------------------------------------------------- 1 | import type nacl from 'tweetnacl'; 2 | import { RuntimeConstants } from './runtime'; 3 | import { Verify } from './Verify'; 4 | 5 | export class VerifyNode extends Verify { 6 | private nacl?: nacl; 7 | 8 | override async verify(body: string, signature: string, timestamp: string): Promise { 9 | if (!this.nacl) { 10 | RuntimeConstants.isNode && (this.nacl = (await import('tweetnacl')).default); 11 | } 12 | 13 | try { 14 | return this.nacl!.sign.detached.verify( 15 | Buffer.from(timestamp + body), 16 | Buffer.from(signature, 'hex'), 17 | Buffer.from(this.publicKey, 'hex'), 18 | ); 19 | } catch { 20 | return false; 21 | } 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /packages/disploy/src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './DiscordAPIUtils'; 2 | export * from './Logger'; 3 | export * from './RequestorError'; 4 | export * from './runtime'; 5 | export * from './SnowflakeUtil'; 6 | export * from './Verify'; 7 | export * from './VerifyCFW'; 8 | export * from './VerifyNode'; 9 | -------------------------------------------------------------------------------- /packages/disploy/src/utils/runtime.ts: -------------------------------------------------------------------------------- 1 | const isNode = typeof process !== 'undefined' && process.versions != null && process.versions.node != null; 2 | 3 | export const RuntimeConstants = { 4 | isNode, 5 | }; 6 | -------------------------------------------------------------------------------- /packages/disploy/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "include": ["./src"], 4 | "compilerOptions": { 5 | "outDir": "./dist" 6 | }, 7 | "exclude": ["dist", "build", "node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/disploy/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { createTsupConfig } from '../../tsup.config.js'; 2 | 3 | export default [ 4 | createTsupConfig({ 5 | entry: ['src/index.ts'], 6 | format: ['esm'], 7 | }), 8 | createTsupConfig({ 9 | entry: ['cli/src/disploy.ts'], 10 | format: ['esm'], 11 | outDir: 'dist/cli', 12 | dts: false, 13 | }), 14 | ]; 15 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ["next", "turbo", "prettier"], 3 | rules: { 4 | "@next/next/no-html-link-for-pages": "off", 5 | "react/jsx-key": "off", 6 | }, 7 | }; 8 | -------------------------------------------------------------------------------- /packages/eslint-config-custom/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "eslint-config-custom", 3 | "version": "0.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "eslint": "^8.30.0", 8 | "eslint-config-next": "^13.1.1", 9 | "eslint-config-prettier": "^8.5.0", 10 | "eslint-config-turbo": "^0.0.7", 11 | "eslint-plugin-react": "7.31.11" 12 | }, 13 | "devDependencies": { 14 | "typescript": "^4.9.4" 15 | }, 16 | "publishConfig": { 17 | "access": "public" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/rest/.cliff-jumperrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json", 3 | "name": "disploy", 4 | "packagePath": "packages/disploy", 5 | "tagTemplate": "{{new-version}}" 6 | } 7 | -------------------------------------------------------------------------------- /packages/rest/CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disploy/disploy/78c947745b2e83f792e51f7251f0e5edf9395028/packages/rest/CHANGELOG.md -------------------------------------------------------------------------------- /packages/rest/README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | disploy 5 |

6 |

7 | Vercel 8 |

9 |

10 | @disploy/rest 11 |

12 |
13 |

14 | Disploy's Discord server 15 |

16 | 17 |
18 | 19 | ## Overview 20 | 21 | `@disploy/rest` 22 | -------------------------------------------------------------------------------- /packages/rest/cliff.toml: -------------------------------------------------------------------------------- 1 | [git] 2 | conventional_commits = true 3 | filter_unconventional = true 4 | split_commits = false 5 | commit_parsers = [ 6 | { message = "^feat(disploy)", group = "Features" }, 7 | { message = "^fix(disploy)", group = "Bug Fixes" }, 8 | { message = "^doc(disploy)", group = "Documentation" }, 9 | { message = "^perf(disploy)", group = "Performance" }, 10 | { message = "^refactor(disploy)", group = "Refactor" }, 11 | { message = "^style(disploy)", group = "Styling" }, 12 | { message = "^test(disploy)", group = "Testing" }, 13 | ] 14 | protect_breaking_commits = false 15 | filter_commits = false 16 | tag_pattern = "v[0-9]*" 17 | skip_tags = "v0.1.0-beta.1" 18 | ignore_tags = "" 19 | date_order = false 20 | sort_commits = "oldest" 21 | link_parsers = [ 22 | { pattern = "#(\\d+)", href = "https://github.com/Disploy/disploy/issues/$1" }, 23 | ] 24 | limit_commits = 42 25 | -------------------------------------------------------------------------------- /packages/rest/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@disploy/rest", 3 | "version": "1.0.0", 4 | "license": "Apache-2.0", 5 | "main": "./dist/index.js", 6 | "source": "./src/index.ts", 7 | "types": "./dist/index.d.ts", 8 | "type": "module", 9 | "files": [ 10 | "dist" 11 | ], 12 | "contributors": [ 13 | "Tristan Camejo " 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Disploy/disploy.git" 18 | }, 19 | "homepage": "https://disploy.dev", 20 | "scripts": { 21 | "build": "tsup", 22 | "dev": "tsup --watch", 23 | "type-check": "tsc --noEmit", 24 | "lint": "TIMING=1 eslint src/**/*.ts* --fix", 25 | "release": "cliff-jumper" 26 | }, 27 | "devDependencies": { 28 | "@favware/cliff-jumper": "^1.9.0", 29 | "@types/node": "^18.11.17", 30 | "eslint": "8.30.0", 31 | "eslint-config-custom": "workspace:^", 32 | "tsconfig": "workspace:^", 33 | "tsup": "^6.5.0", 34 | "typescript": "^4.9.4" 35 | }, 36 | "dependencies": { 37 | "eventemitter3": "^5.0.0" 38 | }, 39 | "publishConfig": { 40 | "access": "public" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/rest/src/Constants.ts: -------------------------------------------------------------------------------- 1 | import type { OptionalRestConfig } from './Rest'; 2 | 3 | export const DefaultRestConfig: Required = { 4 | apiRoot: 'https://discord.com/api/v10', 5 | cacheMatchers: [/^\/gateway\/bot$/], 6 | }; 7 | -------------------------------------------------------------------------------- /packages/rest/src/Rest.ts: -------------------------------------------------------------------------------- 1 | import EventEmitter from 'eventemitter3'; 2 | import { DefaultRestConfig } from './Constants'; 3 | import type { RestEvents } from './types'; 4 | 5 | /** 6 | * Required Configuration for the REST client. 7 | */ 8 | export interface RequiredRestConfig { 9 | token: string; 10 | } 11 | 12 | /** 13 | * Optional Configuration for the REST client. 14 | */ 15 | export interface OptionalRestConfig { 16 | apiRoot?: string; 17 | cacheMatchers?: RegExp[]; 18 | } 19 | 20 | export class Rest extends EventEmitter { 21 | private options: RequiredRestConfig & Required; 22 | private cache: Map = new Map(); 23 | 24 | public constructor(config: RequiredRestConfig & Partial) { 25 | super(); 26 | 27 | this.options = { 28 | ...DefaultRestConfig, 29 | ...config, 30 | }; 31 | } 32 | 33 | private debug(msg: string) { 34 | this.emit('debug', msg); 35 | } 36 | 37 | private async _request(method: string, path: string, body?: any): Promise { 38 | const now = Date.now(); 39 | 40 | const res = await fetch(`${this.options.apiRoot}${path}`, { 41 | method, 42 | headers: { 43 | Authorization: `Bot ${this.options.token}`, 44 | 'Content-Type': 'application/json', 45 | }, 46 | body: JSON.stringify(body), 47 | }); 48 | 49 | this.debug(`[REST] ${method} ${path} (${res.status}) ${Date.now() - now}ms`); 50 | 51 | if (res.status >= 400) { 52 | throw new Error(`${method} ${path} returned ${res.status} ${res.statusText}`); 53 | } 54 | 55 | const contentType = res.headers.get('content-type'); 56 | 57 | if (contentType && contentType.includes('application/json')) { 58 | return res.json(); 59 | } 60 | 61 | return res.arrayBuffer() as unknown as T; 62 | } 63 | 64 | private async request(...args: Parameters): Promise { 65 | const key = args.join(' '); 66 | 67 | if (this.options.cacheMatchers.some((matcher) => matcher.test(args[1]))) { 68 | if (this.cache.has(key)) { 69 | return this.cache.get(key) as T; 70 | } 71 | 72 | const res = await this._request(...args); 73 | this.cache.set(key, res); 74 | return res; 75 | } 76 | 77 | return this._request(...args); 78 | } 79 | 80 | public async get(path: string): Promise { 81 | return this.request('GET', path); 82 | } 83 | 84 | public async post(path: string, body?: REQ): Promise { 85 | return this.request('POST', path, body); 86 | } 87 | 88 | public async patch(path: string, body?: REQ): Promise { 89 | return this.request('PATCH', path, body); 90 | } 91 | 92 | public async delete(path: string): Promise { 93 | return this.request('DELETE', path); 94 | } 95 | 96 | public async put(path: string, body?: REQ): Promise { 97 | return this.request('PUT', path, body); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /packages/rest/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Rest'; 2 | export * from './types'; 3 | -------------------------------------------------------------------------------- /packages/rest/src/types/RestEvents.ts: -------------------------------------------------------------------------------- 1 | export interface RestEvents { 2 | debug: [string]; 3 | } 4 | -------------------------------------------------------------------------------- /packages/rest/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export * from './RestEvents'; 2 | -------------------------------------------------------------------------------- /packages/rest/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "include": ["./src"], 4 | "compilerOptions": { 5 | "outDir": "./dist" 6 | }, 7 | "exclude": ["dist", "build", "node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/rest/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { createTsupConfig } from '../../tsup.config.js'; 2 | 3 | export default createTsupConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['esm'], 6 | }); 7 | -------------------------------------------------------------------------------- /packages/tsconfig/README.md: -------------------------------------------------------------------------------- 1 | # `tsconfig` 2 | 3 | These are base shared `tsconfig.json`s from which all other `tsconfig.json`'s inherit from. 4 | -------------------------------------------------------------------------------- /packages/tsconfig/base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Default", 4 | "compilerOptions": { 5 | "allowUnreachableCode": false, 6 | "allowUnusedLabels": false, 7 | "exactOptionalPropertyTypes": false, 8 | "noFallthroughCasesInSwitch": true, 9 | "noImplicitOverride": true, 10 | "noImplicitReturns": true, 11 | "noUnusedLocals": true, 12 | "noUnusedParameters": true, 13 | "strict": true, 14 | "useUnknownInCatchVariables": true, 15 | "noUncheckedIndexedAccess": true, 16 | "module": "es2020", 17 | "importHelpers": true, 18 | "moduleResolution": "node", 19 | "importsNotUsedAsValues": "error", 20 | "inlineSources": true, 21 | "newLine": "lf", 22 | "noEmitHelpers": true, 23 | "preserveConstEnums": true, 24 | "removeComments": false, 25 | "sourceMap": true, 26 | "esModuleInterop": true, 27 | "forceConsistentCasingInFileNames": true, 28 | "emitDecoratorMetadata": true, 29 | "experimentalDecorators": true, 30 | "target": "ESNext", 31 | "useDefineForClassFields": true, 32 | "declaration": true, 33 | "declarationMap": true, 34 | "skipLibCheck": true 35 | }, 36 | "exclude": ["node_modules"] 37 | } 38 | -------------------------------------------------------------------------------- /packages/tsconfig/frontend-base.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "FrontendDefault", 4 | "compilerOptions": { 5 | "composite": false, 6 | "declaration": true, 7 | "declarationMap": true, 8 | "esModuleInterop": true, 9 | "forceConsistentCasingInFileNames": true, 10 | "inlineSources": false, 11 | "isolatedModules": true, 12 | "moduleResolution": "node", 13 | "noUnusedLocals": false, 14 | "noUnusedParameters": false, 15 | "preserveWatchOutput": true, 16 | "skipLibCheck": true, 17 | "strict": true 18 | }, 19 | "exclude": ["node_modules"] 20 | } 21 | -------------------------------------------------------------------------------- /packages/tsconfig/nextjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Next.js", 4 | "extends": "./frontend-base.json", 5 | "compilerOptions": { 6 | "target": "es5", 7 | "lib": ["dom", "dom.iterable", "esnext"], 8 | "allowJs": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "forceConsistentCasingInFileNames": true, 12 | "noEmit": true, 13 | "incremental": true, 14 | "esModuleInterop": true, 15 | "module": "esnext", 16 | "resolveJsonModule": true, 17 | "isolatedModules": true, 18 | "jsx": "preserve" 19 | }, 20 | "include": ["src", "next-env.d.ts"], 21 | "exclude": ["node_modules"] 22 | } 23 | -------------------------------------------------------------------------------- /packages/tsconfig/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "tsconfig", 3 | "version": "0.0.0", 4 | "private": true, 5 | "main": "index.js", 6 | "files": [ 7 | "base.json", 8 | "frontend-base.json", 9 | "nextjs.json", 10 | "react-library.json" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /packages/tsconfig/react-library.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "React Library", 4 | "extends": "./frontend-base.json", 5 | "compilerOptions": { 6 | "jsx": "react-jsx", 7 | "lib": ["ES2015"], 8 | "module": "ESNext", 9 | "target": "es6" 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /packages/ws/.cliff-jumperrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/favware/cliff-jumper/main/assets/cliff-jumper.schema.json", 3 | "name": "disploy", 4 | "packagePath": "packages/disploy", 5 | "tagTemplate": "{{new-version}}" 6 | } 7 | -------------------------------------------------------------------------------- /packages/ws/CHANGELOG.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Disploy/disploy/78c947745b2e83f792e51f7251f0e5edf9395028/packages/ws/CHANGELOG.md -------------------------------------------------------------------------------- /packages/ws/README.md: -------------------------------------------------------------------------------- 1 |
2 |
3 |

4 | disploy 5 |

6 |

7 | Vercel 8 |

9 |

10 | @disploy/ws 11 |

12 |
13 |

14 | Disploy's Discord server 15 |

16 | 17 |
18 | 19 | ## Overview 20 | 21 | `@disploy/ws` is a WebSocket extension for Disploy. It is used to communicate with the Discord gateway. 22 | This allows you to receive events from Discord and utilize `@discordjs/voice`. 23 | 24 | ## Workflow 25 | 26 | ```ts 27 | import { Gateway } from '@disploy/ws'; 28 | import { GatewayIntentBits } from 'discord-api-types/v10'; 29 | import { App, expressAdapter } from 'disploy'; 30 | import express from 'express'; 31 | 32 | // Handle environment variables 33 | const clientId = process.env.DISCORD_CLIENT_ID; 34 | const token = process.env.DISCORD_TOKEN; 35 | const publicKey = process.env.DISCORD_PUBLIC_KEY; 36 | 37 | if (!clientId || !token || !publicKey) { 38 | throw new Error('Missing environment variables'); 39 | } 40 | 41 | // Setup Discord application 42 | const app = new App(); 43 | 44 | app.start({ 45 | clientId, 46 | token, 47 | publicKey, 48 | }); 49 | 50 | // Setup gateway connection 51 | app.ws = new Gateway(app, { 52 | intents: GatewayIntentBits.MessageContent | GatewayIntentBits.GuildMessages, 53 | }); 54 | 55 | // Example event listener 56 | app.ws.on('messageCreate', (message) => { 57 | if (message.content === 'ping') { 58 | message.reply({ 59 | content: 'pong', 60 | }); 61 | } 62 | }); 63 | 64 | // Setup interaction server 65 | const interactionServer = express(); 66 | 67 | interactionServer.use(express.json()); 68 | expressAdapter(app, interactionServer); 69 | 70 | interactionServer.listen(3000, () => { 71 | console.log('[interaction server] Listening on port 3000'); 72 | }); 73 | 74 | // Connect to gateway 75 | app.ws.connect(); 76 | 77 | // Types 78 | declare module 'disploy' { 79 | interface App { 80 | ws: Gateway; 81 | } 82 | } 83 | ``` 84 | 85 | You can find more examples in the [example bot](https://github.com/Disploy/disploy/tree/main/apps/example) including the usage of `@discordjs/voice`. 86 | 87 | ## Need Help? 88 | 89 | https://discord.gg/E3z8MDnTWn - Join our Discord server for support and updates! 90 | -------------------------------------------------------------------------------- /packages/ws/cliff.toml: -------------------------------------------------------------------------------- 1 | [git] 2 | conventional_commits = true 3 | filter_unconventional = true 4 | split_commits = false 5 | commit_parsers = [ 6 | { message = "^feat(disploy)", group = "Features" }, 7 | { message = "^fix(disploy)", group = "Bug Fixes" }, 8 | { message = "^doc(disploy)", group = "Documentation" }, 9 | { message = "^perf(disploy)", group = "Performance" }, 10 | { message = "^refactor(disploy)", group = "Refactor" }, 11 | { message = "^style(disploy)", group = "Styling" }, 12 | { message = "^test(disploy)", group = "Testing" }, 13 | ] 14 | protect_breaking_commits = false 15 | filter_commits = false 16 | tag_pattern = "v[0-9]*" 17 | skip_tags = "v0.1.0-beta.1" 18 | ignore_tags = "" 19 | date_order = false 20 | sort_commits = "oldest" 21 | link_parsers = [ 22 | { pattern = "#(\\d+)", href = "https://github.com/Disploy/disploy/issues/$1" }, 23 | ] 24 | limit_commits = 42 25 | -------------------------------------------------------------------------------- /packages/ws/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@disploy/ws", 3 | "version": "0.0.0", 4 | "license": "Apache-2.0", 5 | "main": "./dist/index.js", 6 | "source": "./src/index.ts", 7 | "types": "./dist/index.d.ts", 8 | "type": "module", 9 | "files": [ 10 | "dist" 11 | ], 12 | "contributors": [ 13 | "Tristan Camejo " 14 | ], 15 | "repository": { 16 | "type": "git", 17 | "url": "https://github.com/Disploy/disploy.git" 18 | }, 19 | "homepage": "https://disploy.dev", 20 | "scripts": { 21 | "build": "tsup", 22 | "dev": "tsup --watch", 23 | "type-check": "tsc --noEmit", 24 | "lint": "TIMING=1 eslint src/**/*.ts* --fix", 25 | "release": "cliff-jumper" 26 | }, 27 | "devDependencies": { 28 | "@discordjs/voice": "^0.14.0", 29 | "@favware/cliff-jumper": "^1.9.0", 30 | "@types/node": "^18.11.17", 31 | "disploy": "workspace:^", 32 | "eslint": "8.30.0", 33 | "eslint-config-custom": "workspace:^", 34 | "tsconfig": "workspace:^", 35 | "tsup": "^6.5.0", 36 | "typescript": "^4.9.4" 37 | }, 38 | "dependencies": { 39 | "@discordjs/ws": "^0.6.0", 40 | "discord-api-types": "^0.37.24", 41 | "eventemitter3": "^5.0.0" 42 | }, 43 | "publishConfig": { 44 | "access": "public" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /packages/ws/src/Gateway.ts: -------------------------------------------------------------------------------- 1 | import type { REST } from '@discordjs/rest'; 2 | import type { DiscordGatewayAdapterCreator, DiscordGatewayAdapterLibraryMethods } from '@discordjs/voice'; 3 | import { 4 | OptionalWebSocketManagerOptions, 5 | RequiredWebSocketManagerOptions, 6 | WebSocketManager, 7 | WebSocketShardEvents, 8 | } from '@discordjs/ws'; 9 | import { GatewayDispatchEvents } from 'discord-api-types/v10'; 10 | import type { App } from 'disploy'; 11 | import EventEmitter from 'eventemitter3'; 12 | import LoadEvents from './events/_loader'; 13 | import { GatewayEvents, GatewayStatus } from './types'; 14 | export class Gateway extends EventEmitter { 15 | public ws: WebSocketManager; 16 | public status: GatewayStatus = GatewayStatus.Disconnected; 17 | 18 | private voiceAdapters: Map = new Map(); 19 | 20 | /** 21 | * ## Gateway extension for Disploy 22 | * 23 | * Includes an event emitter with abstracted and raw events and a voice adapter for `@discordjs/voice`. 24 | * 25 | * @usage ```ts 26 | * app.start(...) 27 | * 28 | * const gateway = new Gateway(app, { 29 | * intents: GatewayIntentBits.MessageContent | GatewayIntentBits.GuildMessages, 30 | *}); 31 | * 32 | * gateway.on(...) 33 | * 34 | * gateway.connect(); 35 | *``` 36 | */ 37 | constructor( 38 | public readonly app: App, 39 | options: Omit & RequiredWebSocketManagerOptions, 'token' | 'rest'>, 40 | ) { 41 | super(); 42 | this.ws = new WebSocketManager({ ...options, token: app.token, rest: app.rest as unknown as REST }); 43 | this.applyHooks(); 44 | } 45 | 46 | public async connect() { 47 | this.status = GatewayStatus.Connecting; 48 | await this.ws.connect(); 49 | } 50 | 51 | private applyHooks() { 52 | LoadEvents(this); 53 | 54 | this.ws.on(WebSocketShardEvents.Ready, () => { 55 | this.status = GatewayStatus.Connected; 56 | }); 57 | 58 | this.ws.on(WebSocketShardEvents.Dispatch, (packet) => { 59 | this.emit('raw', packet); 60 | 61 | switch (packet.data.t) { 62 | case GatewayDispatchEvents.VoiceServerUpdate: { 63 | if (!packet.data.d.guild_id) break; 64 | 65 | if (this.voiceAdapters.has(packet.data.d.guild_id)) { 66 | this.voiceAdapters.get(packet.data.d.guild_id)?.onVoiceServerUpdate(packet.data.d); 67 | } 68 | break; 69 | } 70 | case GatewayDispatchEvents.VoiceStateUpdate: { 71 | if (!packet.data.d.guild_id) break; 72 | 73 | if (this.voiceAdapters.has(packet.data.d.guild_id)) { 74 | this.voiceAdapters.get(packet.data.d.guild_id)?.onVoiceStateUpdate(packet.data.d); 75 | } 76 | break; 77 | } 78 | } 79 | }); 80 | } 81 | 82 | /** 83 | * Create a `@discordjs/voice` for the Disploy Gateway extension. 84 | * @param guildId The guild's id this adapter is for 85 | * @returns a `@discordjs/voice` for the Disploy Gateway extension. 86 | */ 87 | public createVoiceAdapter(guildId: string): DiscordGatewayAdapterCreator { 88 | return (methods) => { 89 | this.voiceAdapters.set(guildId, methods); 90 | 91 | return { 92 | sendPayload: (payload) => { 93 | if (this.status !== GatewayStatus.Connected) return false; 94 | 95 | const shardId = 0; 96 | this.ws.send(shardId, payload); 97 | return true; 98 | }, 99 | destroy: () => { 100 | this.voiceAdapters.delete(guildId); 101 | }, 102 | }; 103 | }; 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /packages/ws/src/events/ChannelCreate.ts: -------------------------------------------------------------------------------- 1 | import { GatewayChannelCreateDispatch, GatewayDispatchEvents } from 'discord-api-types/v10'; 2 | import type { InternalEventHandler } from '../types'; 3 | 4 | export const ChannelCreate: InternalEventHandler = { 5 | type: GatewayDispatchEvents.ChannelCreate, 6 | 7 | handle(packet, app, emit) { 8 | emit('channelCreate', app.channels.constructChannel(packet.data.d)); 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ws/src/events/ChannelDelete.ts: -------------------------------------------------------------------------------- 1 | import { GatewayChannelDeleteDispatch, GatewayDispatchEvents } from 'discord-api-types/v10'; 2 | import type { InternalEventHandler } from '../types'; 3 | 4 | export const ChannelDelete: InternalEventHandler = { 5 | type: GatewayDispatchEvents.ChannelDelete, 6 | 7 | handle(packet, app, emit) { 8 | emit('channelDelete', app.channels.constructChannel(packet.data.d)); 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ws/src/events/GuildCreate.ts: -------------------------------------------------------------------------------- 1 | import { GatewayDispatchEvents, GatewayGuildCreateDispatch } from 'discord-api-types/v10'; 2 | import { Guild } from 'disploy'; 3 | import type { InternalEventHandler } from '../types'; 4 | 5 | export const GuildCreate: InternalEventHandler = { 6 | type: GatewayDispatchEvents.GuildCreate, 7 | 8 | handle(packet, app, emit) { 9 | emit('guildCreate', new Guild(app, packet.data.d)); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/ws/src/events/GuildDelete.ts: -------------------------------------------------------------------------------- 1 | import { GatewayDispatchEvents, GatewayGuildDeleteDispatch } from 'discord-api-types/v10'; 2 | import type { InternalEventHandler } from '../types'; 3 | 4 | export const GuildDelete: InternalEventHandler = { 5 | type: GatewayDispatchEvents.GuildDelete, 6 | 7 | handle(packet, _app, emit) { 8 | emit('guildDelete', packet.data.d.id); 9 | }, 10 | }; 11 | -------------------------------------------------------------------------------- /packages/ws/src/events/GuildMemberAdd.ts: -------------------------------------------------------------------------------- 1 | import { GatewayDispatchEvents, GatewayGuildMemberAddDispatch } from 'discord-api-types/v10'; 2 | import { GuildMember } from 'disploy'; 3 | import type { InternalEventHandler } from '../types'; 4 | 5 | export const GuildMemberAdd: InternalEventHandler = { 6 | type: GatewayDispatchEvents.GuildMemberAdd, 7 | 8 | handle(packet, app, emit) { 9 | emit('guildMemberAdd', new GuildMember(app, packet.data.d)); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/ws/src/events/GuildMemberRemove.ts: -------------------------------------------------------------------------------- 1 | import { GatewayDispatchEvents, GatewayGuildMemberRemoveDispatch } from 'discord-api-types/v10'; 2 | import { PartialGuildMember } from 'disploy'; 3 | import type { InternalEventHandler } from '../types'; 4 | 5 | export const GuildMemberRemove: InternalEventHandler = { 6 | type: GatewayDispatchEvents.GuildMemberRemove, 7 | 8 | handle(packet, app, emit) { 9 | emit('guildMemberRemove', new PartialGuildMember(app, packet.data.d)); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/ws/src/events/MessageCreate.ts: -------------------------------------------------------------------------------- 1 | import { GatewayDispatchEvents, GatewayMessageCreateDispatch } from 'discord-api-types/v10'; 2 | import { Message } from 'disploy'; 3 | import type { InternalEventHandler } from '../types'; 4 | 5 | export const MessageCreate: InternalEventHandler = { 6 | type: GatewayDispatchEvents.MessageCreate, 7 | 8 | handle(packet, app, emit) { 9 | emit('messageCreate', new Message(app, packet.data.d)); 10 | }, 11 | }; 12 | -------------------------------------------------------------------------------- /packages/ws/src/events/_loader.ts: -------------------------------------------------------------------------------- 1 | import { WebSocketShardEvents } from '@discordjs/ws'; 2 | import type { GatewayDispatchEvents } from 'discord-api-types/v10'; 3 | import type { Gateway } from '../Gateway'; 4 | import type { InternalEventHandler } from '../types'; 5 | import { ChannelCreate } from './ChannelCreate'; 6 | import { ChannelDelete } from './ChannelDelete'; 7 | import { GuildCreate } from './GuildCreate'; 8 | import { GuildDelete } from './GuildDelete'; 9 | import { GuildMemberAdd } from './GuildMemberAdd'; 10 | import { GuildMemberRemove } from './GuildMemberRemove'; 11 | import { MessageCreate } from './MessageCreate'; 12 | 13 | const Events: InternalEventHandler[] = [ 14 | MessageCreate, 15 | GuildCreate, 16 | ChannelCreate, 17 | ChannelDelete, 18 | GuildDelete, 19 | GuildMemberAdd, 20 | GuildMemberRemove, 21 | ]; 22 | 23 | export default function (gateway: Gateway) { 24 | const typeToHandler = new Map>(); 25 | 26 | for (const event of Events) { 27 | typeToHandler.set(event.type, event); 28 | } 29 | 30 | gateway.ws.on(WebSocketShardEvents.Dispatch, (p) => 31 | typeToHandler.get(p.data.t)?.handle(p, gateway.app, gateway.emit.bind(gateway)), 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /packages/ws/src/index.ts: -------------------------------------------------------------------------------- 1 | export * from './Gateway'; 2 | export { GatewayEvents, GatewayStatus } from './types'; 3 | -------------------------------------------------------------------------------- /packages/ws/src/types.ts: -------------------------------------------------------------------------------- 1 | import type { GatewayDispatchEvents, GatewayDispatchPayload } from 'discord-api-types/v10'; 2 | import type { App, DiscordChannel, Guild, GuildMember, Message, PartialGuildMember } from 'disploy'; 3 | import type { Gateway } from './Gateway'; 4 | 5 | export interface GatewayEvents { 6 | raw: [unknown]; 7 | 8 | messageCreate: [Message]; 9 | guildCreate: [Guild]; 10 | channelCreate: [DiscordChannel]; 11 | channelDelete: [DiscordChannel]; 12 | // TODO: Implement some sort of caching so that we can emit the guild before deletion 13 | guildDelete: [string]; 14 | guildMemberAdd: [GuildMember]; 15 | guildMemberRemove: [PartialGuildMember]; 16 | } 17 | 18 | export enum GatewayStatus { 19 | Connecting = 'CONNECTING', 20 | Connected = 'CONNECTED', 21 | Disconnected = 'DISCONNECTED', 22 | } 23 | 24 | export interface InternalEventHandler { 25 | type: GatewayDispatchEvents; 26 | handle( 27 | packet: { 28 | data: T; 29 | } & { 30 | shardId: number; 31 | }, 32 | app: App, 33 | emit: Gateway['emit'], 34 | ): void; 35 | } 36 | -------------------------------------------------------------------------------- /packages/ws/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "tsconfig/base.json", 3 | "include": ["./src"], 4 | "compilerOptions": { 5 | "outDir": "./dist" 6 | }, 7 | "exclude": ["dist", "build", "node_modules"] 8 | } 9 | -------------------------------------------------------------------------------- /packages/ws/tsup.config.js: -------------------------------------------------------------------------------- 1 | import { createTsupConfig } from '../../tsup.config.js'; 2 | 3 | export default createTsupConfig({ 4 | entry: ['src/index.ts'], 5 | format: ['esm'], 6 | }); 7 | -------------------------------------------------------------------------------- /scripts/ayumi.mjs: -------------------------------------------------------------------------------- 1 | import { execSync, spawn } from 'node:child_process'; 2 | import { readFileSync } from 'node:fs'; 3 | import yargs from 'yargs'; 4 | import { hideBin } from 'yargs/helpers'; 5 | 6 | const Packages = ['packages/disploy']; 7 | const ShortGitHash = execSync('git rev-parse --short HEAD').toString().trim(); 8 | 9 | async function run(script) { 10 | const [command, ...args] = script.split(' '); 11 | return new Promise((resolve, reject) => { 12 | spawn(command, args, { stdio: 'inherit' }).on('exit', (code) => { 13 | if (code === 0) { 14 | resolve(); 15 | } else { 16 | reject(); 17 | } 18 | }); 19 | }); 20 | } 21 | 22 | const build = () => run('yarn build'); 23 | 24 | async function publishPackage(path, versionOverride, tag) { 25 | const { version } = JSON.parse(readFileSync(`${path}/package.json`)); 26 | 27 | await run( 28 | `yarn publish ${path} --no-git-tag-version --new-version ${versionOverride || version} ${ 29 | tag ? `--tag ${tag}` : '' 30 | }`, 31 | ); 32 | } 33 | 34 | const publishDevPackage = (path) => publishPackage(path, `0.0.0-${ShortGitHash}`, 'dev'); 35 | 36 | yargs(hideBin(process.argv)) 37 | .command( 38 | 'publish', 39 | '🪄 Publish packages', 40 | () => {}, 41 | async (argv) => { 42 | const { dev } = argv; 43 | 44 | await build(); 45 | 46 | if (dev) { 47 | await Promise.all(Packages.map(publishDevPackage)); 48 | return; 49 | } 50 | 51 | await Promise.all(Packages.map(publishPackage)); 52 | }, 53 | ) 54 | .options('dev', { 55 | alias: 'd', 56 | type: 'boolean', 57 | description: '🔎 Publish dev packages', 58 | }) 59 | .demandCommand(1) 60 | .parse(); 61 | -------------------------------------------------------------------------------- /scripts/benchmark.mjs: -------------------------------------------------------------------------------- 1 | import { spawn } from 'child_process'; 2 | import { dirname, join } from 'path'; 3 | import { fileURLToPath } from 'url'; 4 | 5 | const __filename = fileURLToPath(import.meta.url); 6 | const __dirname = dirname(__filename); 7 | 8 | const getEnv = (name) => { 9 | const value = process.env[name]; 10 | if (value === '' || value === undefined) { 11 | return undefined; 12 | } 13 | return value; 14 | }; 15 | 16 | const parsePullRequestId = (githubRef) => { 17 | const result = /refs\/pull\/(\d+)\/merge/g.exec(githubRef); 18 | if (!result) throw new Error('Reference not found.'); 19 | const [, pullRequestId] = result; 20 | return pullRequestId; 21 | }; 22 | 23 | const Environment = { 24 | DISCORD_TOKEN: getEnv('DISCORD_TOKEN') ?? '_token_', 25 | DISCORD_CLIENT_ID: getEnv('DISCORD_CLIENT_ID') ?? '0', 26 | GITHUB_TOKEN: getEnv('GITHUB_TOKEN'), 27 | GITHUB_REF: getEnv('GITHUB_REF'), 28 | }; 29 | 30 | const server = spawn('yarn', ['workspace', '@disploy/example', 'test-server'], { 31 | env: { 32 | ...process.env, 33 | ...Environment, 34 | }, 35 | }); 36 | 37 | server.on('error', (error) => { 38 | console.error(error); 39 | process.exit(1); 40 | }); 41 | 42 | server.stdout.on('data', (data) => { 43 | if (data.includes('Server Ready!')) { 44 | const args = ['disbench', 'internal', 'benchmark', '-d', '-u', 'http://localhost:5002/interactions']; 45 | 46 | if (Environment.GITHUB_REF) { 47 | args.push('-g'); 48 | args.push(`Disploy/disploy#${parsePullRequestId(Environment.GITHUB_REF)}`); 49 | } 50 | 51 | const benchmark = spawn('yarn', args, { 52 | cwd: join(__dirname, '..', 'apps', 'example'), 53 | stdio: 'inherit', 54 | }); 55 | 56 | benchmark.on('exit', () => { 57 | server.kill(); 58 | process.exit(0); 59 | }); 60 | } 61 | }); 62 | -------------------------------------------------------------------------------- /scripts/changelog.mjs: -------------------------------------------------------------------------------- 1 | // We will use this script to generate a changelog for v1.0.0. 2 | // From then on, we will use git-cliff to generate changelogs. 3 | import { execSync } from 'node:child_process'; 4 | 5 | const commits = []; 6 | const gitLog = execSync('git log --pretty=format:"%h|%an|%s"').toString().trim().split('\n'); 7 | 8 | for (const commit of gitLog) { 9 | try { 10 | const [hash, author, message] = commit.split('|'); 11 | const type = message.split('(')[0]; 12 | const scope = message.split('(')[1].split(')')[0]; 13 | const commitMessage = message.split(': ')[1]; 14 | let pullRequest = null; 15 | try { 16 | pullRequest = commitMessage.split(' (#')[1].split(')')[0]; 17 | } catch (error) {} 18 | 19 | if (type === 'chore' || type === 'docs') { 20 | continue; 21 | } 22 | 23 | if (scope === 'disploy' || scope === 'framework' || scope === '*') { 24 | commits.push({ 25 | type, 26 | scope, 27 | message: pullRequest ? commitMessage.split(' (#')[0] : commitMessage, 28 | pullRequest, 29 | hash, 30 | author, 31 | }); 32 | } 33 | } catch { 34 | console.log(`Error parsing commit: ${commit}`); 35 | } 36 | } 37 | 38 | const types = new Set(commits.map((commit) => commit.type)); 39 | 40 | console.log('## Changelog'); 41 | 42 | for (const type of types) { 43 | console.log(`### ${type}`); 44 | for (const commit of commits) { 45 | if (commit.type === type) { 46 | console.log( 47 | `- [${commit.message}](https://github.com/Disploy/disploy/commit/${commit.hash}) by ${commit.author} ${ 48 | commit.pullRequest ? `(#${commit.pullRequest})` : '' 49 | }`, 50 | ); 51 | } 52 | } 53 | console.log(''); 54 | } 55 | -------------------------------------------------------------------------------- /tsup.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'tsup'; 2 | 3 | export function createTsupConfig({ 4 | entry = ['src/index.ts'], 5 | external = [], 6 | noExternal = [], 7 | platform = 'node', 8 | format = ['esm', 'cjs'], 9 | target = 'es2022', 10 | skipNodeModulesBundle = true, 11 | clean = false, 12 | shims = true, 13 | minify = false, 14 | splitting = false, 15 | keepNames = true, 16 | dts = true, 17 | sourcemap = true, 18 | esbuildPlugins = [], 19 | outDir = 'dist', 20 | } = {}) { 21 | return defineConfig({ 22 | entry, 23 | external, 24 | noExternal, 25 | platform, 26 | format, 27 | skipNodeModulesBundle, 28 | target, 29 | clean, 30 | shims, 31 | minify, 32 | splitting, 33 | keepNames, 34 | dts, 35 | sourcemap, 36 | esbuildPlugins, 37 | outDir, 38 | }); 39 | } 40 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turborepo.org/schema.json", 3 | "pipeline": { 4 | "build": { 5 | "dependsOn": ["^build"], 6 | "outputs": ["dist/**", ".next/**", "build/**"] 7 | }, 8 | "type-check": { 9 | "dependsOn": ["^build"], 10 | "outputs": [] 11 | }, 12 | "test": { 13 | "dependsOn": ["^build"], 14 | "outputs": [] 15 | }, 16 | "lint": { 17 | "outputs": [] 18 | }, 19 | "dev": { 20 | "dependsOn": [], 21 | "cache": false, 22 | "env": ["NODE_ENV"] 23 | } 24 | } 25 | } 26 | --------------------------------------------------------------------------------