diff --git a/node/discord-command-bot/.gitignore b/node/discord-command-bot/.gitignore new file mode 100644 index 00000000..6a7d6d8e --- /dev/null +++ b/node/discord-command-bot/.gitignore @@ -0,0 +1,130 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* \ No newline at end of file diff --git a/node/discord-command-bot/.prettierrc.json b/node/discord-command-bot/.prettierrc.json new file mode 100644 index 00000000..0a725205 --- /dev/null +++ b/node/discord-command-bot/.prettierrc.json @@ -0,0 +1,6 @@ +{ + "trailingComma": "es5", + "tabWidth": 2, + "semi": true, + "singleQuote": true +} diff --git a/node/discord-command-bot/README.md b/node/discord-command-bot/README.md new file mode 100644 index 00000000..63cd9f7b --- /dev/null +++ b/node/discord-command-bot/README.md @@ -0,0 +1,61 @@ +# 🤖 Node.js Discord Command Bot Function + +Simple command using Discord Interactions. + +## 🧰 Usage + +### `POST` + +Webhook to receive Discord command events. To receive events, you must register your application as a [Discord bot](https://discord.com/developers/applications). + +**Parameters** + +| Name | Description | Location | Type | Sample Value | +| --------------------- | -------------------------------- | -------- | ------ | --------------- | +| x-signature-ed25519 | Signature of the request payload | Header | string | `d1efb...aec35` | +| x-signature-timestamp | Timestamp of the request payload | Header | string | `1629837700` | + +**Response** + +Sample `200` Response: + +Returns a Discord message object. + +```json +{ + "type": 4, + "data": { + "content": "Hello from Appwrite 👋" + } +} +``` + +Sample `401` Response: + +```json +{ + "error": "Invalid request signature" +} +``` + +## ⚙️ Configuration + +| Setting | Value | +| ----------------- | ------------- | +| Runtime | Node (18.0) | +| Entrypoint | `src/main.js` | +| Build Commands | `npm install` | +| Permissions | `any` | +| Timeout (Seconds) | 15 | + +## 🔒 Environment Variables + +### DISCORD_PUBLIC_KEY + +Discord Public Key to verify request signature. + +| Question | Answer | +| ------------- | ---------------------------------------------------------------------------------------------------------------------- | +| Required | Yes | +| Sample Value | `d1efb...aec35` | +| Documentation | [Discord Docs](https://discord.com/developers/docs/tutorials/hosting-on-cloudflare-workers#creating-an-app-on-discord) | diff --git a/node/discord-command-bot/env.d.ts b/node/discord-command-bot/env.d.ts new file mode 100644 index 00000000..0183aadd --- /dev/null +++ b/node/discord-command-bot/env.d.ts @@ -0,0 +1,9 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + DISCORD_PUBLIC_KEY: string; + } + } +} + +export {}; diff --git a/node/discord-command-bot/package-lock.json b/node/discord-command-bot/package-lock.json new file mode 100644 index 00000000..566332f5 --- /dev/null +++ b/node/discord-command-bot/package-lock.json @@ -0,0 +1,49 @@ +{ + "name": "discord-bot", + "version": "1.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "discord-bot", + "version": "1.0.0", + "dependencies": { + "discord-interactions": "^3.4.0" + }, + "devDependencies": { + "prettier": "^3.0.0" + } + }, + "node_modules/discord-interactions": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/discord-interactions/-/discord-interactions-3.4.0.tgz", + "integrity": "sha512-DG0Jxdd/FcK8liAPhIP4u5YHpnz50JWn9DK4OavxsLD49/WGimXtP3EdOY439MaWyCgQfsfFkA1GsTEyu63RzA==", + "dependencies": { + "tweetnacl": "^1.0.3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/prettier": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.0.0.tgz", + "integrity": "sha512-zBf5eHpwHOGPC47h0zrPyNn+eAEIdEzfywMoYn2XPi0P44Zp0tSq64rq0xAREh4auw2cJZHo9QUob+NqCQky4g==", + "dev": true, + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + } + } +} diff --git a/node/discord-command-bot/package.json b/node/discord-command-bot/package.json new file mode 100644 index 00000000..ad9c6f04 --- /dev/null +++ b/node/discord-command-bot/package.json @@ -0,0 +1,16 @@ +{ + "name": "discord-bot", + "version": "1.0.0", + "description": "", + "main": "src/main.js", + "type": "module", + "scripts": { + "format": "prettier --write ." + }, + "dependencies": { + "discord-interactions": "^3.4.0" + }, + "devDependencies": { + "prettier": "^3.0.0" + } +} diff --git a/node/discord-command-bot/src/main.js b/node/discord-command-bot/src/main.js new file mode 100644 index 00000000..1ca15146 --- /dev/null +++ b/node/discord-command-bot/src/main.js @@ -0,0 +1,33 @@ +import { InteractionResponseType, InteractionType } from 'discord-interactions'; +import { throwIfMissing } from './utils.js'; + +export default async ({ req, res, error }) => { + throwIfMissing(process.env, ['DISCORD_PUBLIC_KEY']); + + const verified = await verifyKey( + req.bodyRaw, + req.headers['x-signature-ed25519'], + req.headers['x-signature-timestamp'], + process.env.DISCORD_PUBLIC_KEY + ); + + if (!verified) { + error('Invalid request.'); + return res.json({ error: 'Invalid request signature' }, 401); + } + + const interaction = req.body; + if ( + interaction.type === InteractionType.APPLICATION_COMMAND && + interaction.data.name === 'hello' + ) { + return res.json({ + type: InteractionResponseType.CHANNEL_MESSAGE_WITH_SOURCE, + data: { + content: 'Hello from Appwrite 👋', + }, + }); + } + + return res.json({ type: InteractionResponseType.PONG }); +}; diff --git a/node/discord-command-bot/src/utils.js b/node/discord-command-bot/src/utils.js new file mode 100644 index 00000000..dcca7015 --- /dev/null +++ b/node/discord-command-bot/src/utils.js @@ -0,0 +1,17 @@ +/** + * Throws an error if any of the keys are missing from the object + * @param {*} obj + * @param {string[]} keys + * @throws {Error} + */ +export function throwIfMissing(obj, keys) { + const missing = []; + for (let key of keys) { + if (!(key in obj) || !obj[key]) { + missing.push(key); + } + } + if (missing.length > 0) { + throw new Error(`Missing required fields: ${missing.join(', ')}`); + } +}