├── .env.example ├── src ├── commands │ ├── index.ts │ ├── info │ │ ├── ping.ts │ │ └── hello.ts │ ├── fun │ │ └── cats.ts │ └── buttons │ │ ├── button.ts │ │ ├── modal.ts │ │ └── list.ts └── index.ts ├── wrangler.jsonc ├── package.json ├── slash-up.config.js ├── tsconfig.json ├── README.md └── .gitignore /.env.example: -------------------------------------------------------------------------------- 1 | DISCORD_APP_ID= 2 | DISCORD_PUBLIC_KEY= 3 | DISCORD_BOT_TOKEN= 4 | 5 | # You can fill this in for development.env 6 | DEVELOPMENT_GUILD_ID= -------------------------------------------------------------------------------- /src/commands/index.ts: -------------------------------------------------------------------------------- 1 | export const commands = [ 2 | // info folder 3 | require("./info/hello"), 4 | require('./info/ping'), 5 | // fun folder 6 | require("./fun/cats"), 7 | // buttons folder 8 | require("./buttons/button"), 9 | require("./buttons/modal"), 10 | require("./buttons/list"), 11 | ] -------------------------------------------------------------------------------- /wrangler.jsonc: -------------------------------------------------------------------------------- 1 | /** 2 | * For more details on how to configure Wrangler, refer to: 3 | * https://developers.cloudflare.com/workers/wrangler/configuration/ 4 | */ 5 | { 6 | "$schema": "node_modules/wrangler/config-schema.json", 7 | "name": "cloudflare-worker-discord", 8 | "main": "src/index.ts", 9 | "compatibility_date": "2025-07-30", 10 | "observability": { 11 | "enabled": true 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/commands/info/ping.ts: -------------------------------------------------------------------------------- 1 | import { SlashCommand, SlashCreator, CommandContext } from 'slash-create/web'; 2 | 3 | export default class BotCommand extends SlashCommand { 4 | constructor(creator: SlashCreator) { 5 | super(creator, { 6 | name: 'ping', 7 | description: 'pong!' 8 | }); 9 | } 10 | 11 | async run(ctx: CommandContext) { 12 | return `🏓 Pong!`; 13 | } 14 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cloudflare-workers-discord", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "deploy": "wrangler deploy", 7 | "dev": "wrangler dev", 8 | "start": "wrangler dev", 9 | "cf-typegen": "wrangler types", 10 | "sync": "slash-up sync", 11 | "sync:dev": "slash-up sync -e development" 12 | }, 13 | "dependencies": { 14 | "@discordjs/builders": "^1.11.2", 15 | "discord-api-types": "^0.38.18", 16 | "hono": "^4.8.10" 17 | }, 18 | "devDependencies": { 19 | "@types/node": "22.13.0", 20 | "@types/service-worker-mock": "^2.0.4", 21 | "slash-create": "^6.5.0", 22 | "slash-up": "^1.4.2", 23 | "wrangler": "^4.26.1" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /slash-up.config.js: -------------------------------------------------------------------------------- 1 | // This is the slash-up config file. 2 | // Make sure to fill in "token" and "applicationId" before using. 3 | // You can also use environment variables from the ".env" file if any. 4 | 5 | module.exports = { 6 | // The Token of the Discord bot 7 | token: process.env.DISCORD_BOT_TOKEN, 8 | // The Application ID of the Discord bot 9 | applicationId: process.env.DISCORD_APP_ID, 10 | // This is where the path to command files are, .ts files are supported! 11 | commandPath: './src/commands', 12 | // You can use different environments with --env (-e) 13 | env: { 14 | development: { 15 | // The "globalToGuild" option makes global commands sync to the specified guild instead. 16 | globalToGuild: process.env.DEVELOPMENT_GUILD_ID 17 | } 18 | } 19 | }; -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Base Options: */ 4 | "esModuleInterop": true, 5 | "skipLibCheck": true, 6 | "target": "es2022", 7 | "verbatimModuleSyntax": false, 8 | "allowJs": true, 9 | "resolveJsonModule": true, 10 | "moduleDetection": "force", 11 | /* Strictness */ 12 | "noImplicitAny": false, 13 | "noImplicitThis": true, 14 | "strictNullChecks": false, 15 | "strict": true, 16 | "noUncheckedIndexedAccess": true, 17 | /* If NOT transpiling with TypeScript: */ 18 | "moduleResolution": "Bundler", 19 | "module": "es2022", 20 | "noEmit": true, 21 | "lib": ["es2022"], 22 | "types": [ 23 | "./worker-configuration.d.ts", 24 | "@types/node", 25 | "@types/service-worker-mock" 26 | ] 27 | }, 28 | "exclude": ["node_modules", "dist", "tests"], 29 | "include": ["src", "worker-configuration.d.ts"] 30 | } 31 | -------------------------------------------------------------------------------- /src/commands/info/hello.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from '@discordjs/builders'; 2 | import { SlashCommand, CommandOptionType, SlashCreator, CommandContext } from 'slash-create/web'; 3 | 4 | export default class BotCommand extends SlashCommand { 5 | constructor(creator: SlashCreator) { 6 | super(creator, { 7 | name: 'hello', 8 | description: 'Says hello to you.', 9 | options: [ 10 | { 11 | type: CommandOptionType.STRING, 12 | name: 'food', 13 | description: 'What food do you like?' 14 | } 15 | ] 16 | }); 17 | } 18 | 19 | async run(ctx: CommandContext) { 20 | 21 | let embed = new EmbedBuilder() 22 | .setDescription(ctx.options.food ? `You like ${ctx.options.food}? Nice!` : `Hello, ${ctx.user.username}!`) 23 | 24 | return ctx.send({ embeds: [embed.toJSON()] }); 25 | } 26 | } -------------------------------------------------------------------------------- /src/commands/fun/cats.ts: -------------------------------------------------------------------------------- 1 | import { EmbedBuilder } from '@discordjs/builders'; 2 | import { SlashCommand, SlashCreator, CommandContext, ApplicationIntegrationType, InteractionContextType } from 'slash-create/web'; 3 | 4 | export default class BotCommand extends SlashCommand { 5 | constructor(creator: SlashCreator) { 6 | super(creator, { 7 | name: 'cats', 8 | description: 'free cats!!', 9 | integrationTypes: [ 10 | ApplicationIntegrationType.GUILD_INSTALL, 11 | ApplicationIntegrationType.USER_INSTALL], 12 | contexts: [ 13 | InteractionContextType.BOT_DM, 14 | InteractionContextType.GUILD, 15 | InteractionContextType.PRIVATE_CHANNEL 16 | ] 17 | }); 18 | } 19 | 20 | async run(ctx: CommandContext) { 21 | 22 | const res = await fetch('https://api.thecatapi.com/v1/images/search'); 23 | const data = await res.json(); 24 | 25 | let embed = new EmbedBuilder() 26 | .setColor(0x0099FF) 27 | .setTitle("Cats!!") 28 | .setImage(data[0].url); 29 | 30 | return ctx.send({ embeds: [embed.toJSON()] }) 31 | } 32 | } -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { commands } from './commands'; 2 | import { SlashCreator, CloudflareWorkerServer } from 'slash-create/web'; 3 | 4 | const cfServer = new CloudflareWorkerServer(); 5 | let creator: SlashCreator; 6 | 7 | // Since we only get our secrets on fetch, set them before running 8 | function makeCreator(env: Record) { 9 | creator = new SlashCreator({ 10 | applicationID: env.DISCORD_APP_ID, 11 | publicKey: env.DISCORD_PUBLIC_KEY, 12 | token: env.DISCORD_BOT_TOKEN 13 | }); 14 | creator.withServer(cfServer).registerCommands(commands); 15 | 16 | creator.on('warn', (message) => console.warn(message)); 17 | creator.on('error', (error) => console.error(error.stack || error.toString())); 18 | creator.on('commandRun', (command, _, ctx) => 19 | console.info(`${ctx.user.username}#${ctx.user.discriminator} (${ctx.user.id}) ran command ${command.commandName}`) 20 | ); 21 | creator.on('commandError', (command, error) => 22 | console.error(`Command ${command.commandName} errored:`, error.stack || error.toString()) 23 | ); 24 | } 25 | 26 | export default { 27 | async fetch(request: Request, env: Env, ctx: ExecutionContext) { 28 | if (!creator) makeCreator(env); 29 | return cfServer.fetch(request, env, ctx); 30 | }, 31 | } -------------------------------------------------------------------------------- /src/commands/buttons/button.ts: -------------------------------------------------------------------------------- 1 | import { ActionRowBuilder, ButtonBuilder } from '@discordjs/builders'; 2 | import { ButtonStyle } from 'discord-api-types/v10'; 3 | import { SlashCommand, SlashCreator, CommandContext } from 'slash-create/web'; 4 | 5 | export default class BotCommand extends SlashCommand { 6 | constructor(creator: SlashCreator) { 7 | super(creator, { 8 | name: 'buttons', 9 | description: 'Shows a button!' 10 | }); 11 | } 12 | 13 | async run(ctx: CommandContext) { 14 | await ctx.defer(); 15 | 16 | let actionRow = new ActionRowBuilder(); 17 | actionRow.addComponents( 18 | new ButtonBuilder({ 19 | custom_id: 'a_button', 20 | style: ButtonStyle.Primary, 21 | label: 'Click Me 1' 22 | }), 23 | new ButtonBuilder({ 24 | custom_id: 'b_button', 25 | style: ButtonStyle.Danger, 26 | label: 'Click Me 2' 27 | }) 28 | ) 29 | 30 | await ctx.send({ 31 | content: "You now have **buttons!!**", 32 | components: [actionRow.toJSON() as any] 33 | }); 34 | 35 | ctx.registerComponent('a_button', async (btnCtx) => { 36 | await btnCtx.send("You clicked the button 1!") 37 | }); 38 | ctx.registerComponent('b_button', async (btnCtx) => { 39 | await btnCtx.send('You clicked the button 2!'); 40 | }); 41 | 42 | } 43 | } -------------------------------------------------------------------------------- /src/commands/buttons/modal.ts: -------------------------------------------------------------------------------- 1 | import { ActionRowBuilder, ModalActionRowComponentBuilder, ModalBuilder, TextInputBuilder } from '@discordjs/builders'; 2 | import { TextInputStyle } from 'discord-api-types/v10'; 3 | import { SlashCommand, SlashCreator, CommandContext } from 'slash-create/web'; 4 | 5 | export default class BotCommand extends SlashCommand { 6 | constructor(creator: SlashCreator) { 7 | super(creator, { 8 | name: 'modal', 9 | description: 'Shows a modal!' 10 | }); 11 | 12 | creator.registerGlobalModal('yourModal', (interact) => { 13 | interact.sendFollowUp(`Hello ${interact.values.yourName}, your favourite food is ${interact.values.yourFood}!`); 14 | }) 15 | } 16 | 17 | async run(ctx: CommandContext) { 18 | 19 | let modal = new ModalBuilder() 20 | .setCustomId('yourModal') 21 | .setTitle('Your Modal') 22 | 23 | let row1 = new ActionRowBuilder().addComponents( 24 | new TextInputBuilder().setCustomId("yourName").setLabel("Your Name").setRequired(true).setStyle(TextInputStyle.Short), 25 | ) 26 | let row2 = new ActionRowBuilder().addComponents( 27 | new TextInputBuilder().setCustomId("yourFood").setLabel("Your Favourite Food").setRequired(true).setStyle(TextInputStyle.Short) 28 | ) 29 | modal.addComponents(row1, row2) 30 | 31 | await ctx.sendModal(modal.toJSON() as any) 32 | } 33 | } -------------------------------------------------------------------------------- /src/commands/buttons/list.ts: -------------------------------------------------------------------------------- 1 | import { ActionRowBuilder, ModalActionRowComponentBuilder, ModalBuilder, SelectMenuBuilder, StringSelectMenuBuilder, StringSelectMenuOptionBuilder, TextInputBuilder } from '@discordjs/builders'; 2 | import { TextInputStyle } from 'discord-api-types/v10'; 3 | import { SlashCommand, SlashCreator, CommandContext } from 'slash-create/web'; 4 | 5 | export default class BotCommand extends SlashCommand { 6 | constructor(creator: SlashCreator) { 7 | super(creator, { 8 | name: 'list', 9 | description: 'Shows a list!' 10 | }); 11 | } 12 | 13 | async run(ctx: CommandContext) { 14 | await ctx.defer() 15 | 16 | let actionrow = new ActionRowBuilder(); 17 | actionrow.addComponents( 18 | new SelectMenuBuilder() 19 | .setCustomId("selectMenu") 20 | .setPlaceholder("Select an option") 21 | .addOptions( 22 | new StringSelectMenuOptionBuilder().setLabel("Option 1").setValue("1").setDescription("This is option 1"), 23 | new StringSelectMenuOptionBuilder().setLabel("Option 2").setValue("2").setDescription("This is option 2"), 24 | new StringSelectMenuOptionBuilder().setLabel("Option 3").setValue("3").setDescription("This is option 3") 25 | ) 26 | ) 27 | 28 | await ctx.send({ 29 | components: [actionrow.toJSON() as any] 30 | }) 31 | 32 | ctx.registerComponent('selectMenu', async (nCtx) => { 33 | console.log(nCtx.values) 34 | await nCtx.send(`You've selected option: \`${nCtx.values[0]}\`!`) 35 | }); 36 | 37 | } 38 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # cloudflare-workers-discord 2 | 3 | A [slash-create](https://npm.im/slash-create) template, using [Cloudflare Workers](https://workers.cloudflare.com). This project is based on [`https://github.com/Snazzah/slash-create-worker`](https://github.com/Snazzah/slash-create-worker) 4 | 5 | ## Getting Started 6 | ### Cloning the repo 7 | To get started with this project, clone the repository: 8 | ```sh 9 | git clone https://github.com/towsifkafi/cloudflare-workers-discord.git 10 | ``` 11 | 12 | After cloning, install the dependencies using npm: 13 | ```sh 14 | npm install 15 | ``` 16 | ### Installing and setting up Wrangler 17 | > Make sure to [sign up for a Cloudflare Workers account](https://dash.cloudflare.com/sign-up/workers) in a browser before continuing. 18 | Install wrangler with npm: 19 | ```sh 20 | npm install -D wrangler@latest 21 | ``` 22 | Read more about [installing wrangler](https://developers.cloudflare.com/workers/cli-wrangler/install-update). 23 | 24 | Afterwards, run `wrangler login` to login to your Cloudflare account with OAuth: 25 | ```sh 26 | wrangler login 27 | ``` 28 | 29 | ### Filling in secrets 30 | You can enter in environment secrets with `wrangler secret put`, here are the keys that are required to run this: 31 | ```sh 32 | npx wrangler secret put DISCORD_APP_ID 33 | npx wrangler secret put DISCORD_PUBLIC_KEY 34 | npx wrangler secret put DISCORD_BOT_TOKEN 35 | ``` 36 | 37 | ### Development 38 | To run this locally, create a `.env` file and a `.dev.vars` file based on the provided examples, then you can run `wrangler dev` to start a local dev environment and use something like ngrok to tunnel it to a URL. 39 | 40 | To sync commands in the development environment, run `npm run sync:dev` (or `yarn sync:dev`). 41 | 42 | > Note: When you create a command, make sure to include it in the array of commands in `./src/commands/index.ts`. 43 | 44 | ### Production 45 | To sync to production, run `npm run sync`. To publish code to a worker, run `npm run deploy`. 46 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # ide 2 | 3 | .idx/ 4 | .vscode/ 5 | 6 | # Logs 7 | 8 | logs 9 | _.log 10 | npm-debug.log_ 11 | yarn-debug.log* 12 | yarn-error.log* 13 | lerna-debug.log* 14 | .pnpm-debug.log* 15 | 16 | # Diagnostic reports (https://nodejs.org/api/report.html) 17 | 18 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json 19 | 20 | # Runtime data 21 | 22 | pids 23 | _.pid 24 | _.seed 25 | \*.pid.lock 26 | 27 | # Directory for instrumented libs generated by jscoverage/JSCover 28 | 29 | lib-cov 30 | 31 | # Coverage directory used by tools like istanbul 32 | 33 | coverage 34 | \*.lcov 35 | 36 | # nyc test coverage 37 | 38 | .nyc_output 39 | 40 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 41 | 42 | .grunt 43 | 44 | # Bower dependency directory (https://bower.io/) 45 | 46 | bower_components 47 | 48 | # node-waf configuration 49 | 50 | .lock-wscript 51 | 52 | # Compiled binary addons (https://nodejs.org/api/addons.html) 53 | 54 | build/Release 55 | 56 | # Dependency directories 57 | 58 | node_modules/ 59 | jspm_packages/ 60 | 61 | # Snowpack dependency directory (https://snowpack.dev/) 62 | 63 | web_modules/ 64 | 65 | # TypeScript cache 66 | 67 | \*.tsbuildinfo 68 | 69 | # Optional npm cache directory 70 | 71 | .npm 72 | 73 | # Optional eslint cache 74 | 75 | .eslintcache 76 | 77 | # Optional stylelint cache 78 | 79 | .stylelintcache 80 | 81 | # Microbundle cache 82 | 83 | .rpt2_cache/ 84 | .rts2_cache_cjs/ 85 | .rts2_cache_es/ 86 | .rts2_cache_umd/ 87 | 88 | # Optional REPL history 89 | 90 | .node_repl_history 91 | 92 | # Output of 'npm pack' 93 | 94 | \*.tgz 95 | 96 | # Yarn Integrity file 97 | 98 | .yarn-integrity 99 | 100 | # dotenv environment variable files 101 | 102 | .env 103 | .env.development.local 104 | .env.test.local 105 | .env.production.local 106 | .env.local 107 | 108 | # parcel-bundler cache (https://parceljs.org/) 109 | 110 | .cache 111 | .parcel-cache 112 | 113 | # Next.js build output 114 | 115 | .next 116 | out 117 | 118 | # Nuxt.js build / generate output 119 | 120 | .nuxt 121 | dist 122 | 123 | # Gatsby files 124 | 125 | .cache/ 126 | 127 | # Comment in the public line in if your project uses Gatsby and not Next.js 128 | 129 | # https://nextjs.org/blog/next-9-1#public-directory-support 130 | 131 | # public 132 | 133 | # vuepress build output 134 | 135 | .vuepress/dist 136 | 137 | # vuepress v2.x temp and cache directory 138 | 139 | .temp 140 | .cache 141 | 142 | # Docusaurus cache and generated files 143 | 144 | .docusaurus 145 | 146 | # Serverless directories 147 | 148 | .serverless/ 149 | 150 | # FuseBox cache 151 | 152 | .fusebox/ 153 | 154 | # DynamoDB Local files 155 | 156 | .dynamodb/ 157 | 158 | # TernJS port file 159 | 160 | .tern-port 161 | 162 | # Stores VSCode versions used for testing VSCode extensions 163 | 164 | .vscode-test 165 | 166 | # yarn v2 167 | 168 | .yarn/cache 169 | .yarn/unplugged 170 | .yarn/build-state.yml 171 | .yarn/install-state.gz 172 | .pnp.\* 173 | 174 | # wrangler project 175 | 176 | .dev.vars 177 | .wrangler/ 178 | --------------------------------------------------------------------------------