diff --git a/09-dynamic-guilds/.editorconfig b/09-dynamic-guilds/.editorconfig new file mode 100644 index 00000000..ea6cddee --- /dev/null +++ b/09-dynamic-guilds/.editorconfig @@ -0,0 +1,19 @@ +# editorconfig.org +root = true + +[*] +end_of_line = lf +indent_style = tab +charset = utf-8 +trim_trailing_whitespace = true +insert_final_newline = true +indent_size = 4 +max_line_length = 140 + +[*.{yaml, yml}] +indent_style = space +indent_size = 2 + +[*.md] +indent_style = space +trim_trailing_whitespace = false diff --git a/09-dynamic-guilds/.gitignore b/09-dynamic-guilds/.gitignore new file mode 100644 index 00000000..29ee4371 --- /dev/null +++ b/09-dynamic-guilds/.gitignore @@ -0,0 +1,36 @@ +# compiled output +/dist +/node_modules +yarn.lock +yarn-error.log + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# OS +.DS_Store + +# Tests +/coverage +/.nyc_output + +# IDEs and editors +/.idea +.project +.classpath +.c9/ +*.launch +.settings/ +*.sublime-workspace + +# IDE - VSCode +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json diff --git a/09-dynamic-guilds/.prettierrc b/09-dynamic-guilds/.prettierrc new file mode 100644 index 00000000..415f2c45 --- /dev/null +++ b/09-dynamic-guilds/.prettierrc @@ -0,0 +1,8 @@ +{ + "parser": "typescript", + "trailingComma": "none", + "singleQuote": true, + "arrowParens": "avoid", + "endOfLine": "auto", + "printWidth": 120 +} diff --git a/09-dynamic-guilds/README.md b/09-dynamic-guilds/README.md new file mode 100644 index 00000000..9bfb7f13 --- /dev/null +++ b/09-dynamic-guilds/README.md @@ -0,0 +1,3 @@ +# 09 - Dynamic Guilds + +This is an updated version of an example created by [@WolffParkinson](https://github.com/wolffparkinson). You can find the original example [here](https://github.com/wolffparkinson/necord-playground/tree/dynamic-guilds) diff --git a/09-dynamic-guilds/eslint.config.js b/09-dynamic-guilds/eslint.config.js new file mode 100644 index 00000000..bbd28044 --- /dev/null +++ b/09-dynamic-guilds/eslint.config.js @@ -0,0 +1,46 @@ +const typescriptEslintEslintPlugin = require("@typescript-eslint/eslint-plugin"); +const globals = require("globals"); +const tsParser = require("@typescript-eslint/parser"); +const js = require("@eslint/js"); +const { FlatCompat } = require("@eslint/eslintrc"); + +const compat = new FlatCompat({ + baseDirectory: __dirname, + recommendedConfig: js.configs.recommended, + allConfig: js.configs.all +}); + +module.exports = [...compat.extends( + "plugin:@typescript-eslint/eslint-recommended", + "plugin:@typescript-eslint/recommended", + "prettier", +), { + plugins: { + "@typescript-eslint": typescriptEslintEslintPlugin, + }, + + languageOptions: { + globals: { + ...globals.node, + }, + + parser: tsParser, + ecmaVersion: 5, + sourceType: "module", + + parserOptions: { + project: "tsconfig.json", + }, + }, + + rules: { + "@typescript-eslint/interface-name-prefix": "off", + "@typescript-eslint/explicit-function-return-type": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/no-unused-vars": "off", + "@typescript-eslint/ban-types": "off", + }, + + ignores: ["*.config.js"] +}]; \ No newline at end of file diff --git a/09-dynamic-guilds/package.json b/09-dynamic-guilds/package.json new file mode 100644 index 00000000..55ae6fb3 --- /dev/null +++ b/09-dynamic-guilds/package.json @@ -0,0 +1,37 @@ +{ + "name": "09-dynamic-guilds", + "version": "1.0.0", + "license": "MIT", + "scripts": { + "prebuild": "rimraf dist", + "build": "nest build", + "format": "prettier --write \"src/**/*.ts\"", + "start": "nest start", + "start:dev": "nest start --watch", + "start:debug": "nest start --debug --watch", + "start:prod": "node dist/main", + "lint": "eslint src/**/*.ts --fix" + }, + "dependencies": { + "@nestjs/common": "10.2.0", + "@nestjs/core": "10.2.0", + "discord.js": "14.17.3", + "necord": "6.8.7", + "reflect-metadata": "0.2.1", + "rimraf": "4.1.2", + "rxjs": "7.8.0" + }, + "devDependencies": { + "@eslint/eslintrc": "^3.1.0", + "@eslint/js": "^9.10.0", + "@types/node": "18.14.2", + "@typescript-eslint/eslint-plugin": "5.54.0", + "@typescript-eslint/parser": "5.54.0", + "eslint": "9.10.0", + "eslint-config-prettier": "8.6.0", + "eslint-plugin-prettier": "4.2.1", + "globals": "^15.9.0", + "prettier": "2.8.4", + "typescript": "5.7.3" + } +} diff --git a/09-dynamic-guilds/src/app.module.ts b/09-dynamic-guilds/src/app.module.ts new file mode 100644 index 00000000..9bce5b4d --- /dev/null +++ b/09-dynamic-guilds/src/app.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { NecordModule } from 'necord'; +import { CommandService } from './command.service'; +import { DynamicCommand } from './dynamic.command'; +import { AppService } from './app.service'; +import { SimpleCommand } from './simple.command'; + +@Module({ + imports: [ + NecordModule.forRoot({ + intents: ['Guilds'], + token: process.env.DISCORD_TOKEN, + skipRegistration: true + }) + ], + providers: [CommandService, AppService, DynamicCommand, SimpleCommand] +}) +export class AppModule {} diff --git a/09-dynamic-guilds/src/app.service.ts b/09-dynamic-guilds/src/app.service.ts new file mode 100644 index 00000000..83e9c51c --- /dev/null +++ b/09-dynamic-guilds/src/app.service.ts @@ -0,0 +1,12 @@ +import { Injectable, Logger } from '@nestjs/common'; +import { ContextOf, Ctx, Once } from 'necord'; + +@Injectable() +export class AppService { + private readonly logger = new Logger(AppService.name); + + @Once('ready') + public onReady(@Ctx() [client]: ContextOf<'ready'>) { + this.logger.log(`Bot is ready! Logged in as ${client.user.tag}`); + } +} diff --git a/09-dynamic-guilds/src/command.service.ts b/09-dynamic-guilds/src/command.service.ts new file mode 100644 index 00000000..629d687a --- /dev/null +++ b/09-dynamic-guilds/src/command.service.ts @@ -0,0 +1,50 @@ +import { Injectable, Logger, OnApplicationBootstrap } from '@nestjs/common'; +import { CommandsService, ExplorerService, SlashCommandDiscovery, SlashCommandsService, SlashCommand } from 'necord'; +import { Client } from 'discord.js'; + +@Injectable() +export class CommandService implements OnApplicationBootstrap { + private readonly logger = new Logger(); + + constructor( + private readonly slashCommandService: SlashCommandsService, + private readonly explorerService: ExplorerService, + private readonly commandService: CommandsService, + private readonly client: Client + ) {} + + async onApplicationBootstrap() { + this.client.once('ready', async () => { + await this.updateCommandsMeta(); + await this.commandService.registerAllCommands() + }); + } + + // Fetch guild ids from API + async fetchGuildIds() { + return [{ id: 1, name: 'dynamic', guildIds: [process.env.DB_GUILD_ID] }]; + } + + async updateCommandsMeta() { + this.logger.verbose('Updating metadata for SlashCommands'); + + const slashCommands = this.explorerService.explore(SlashCommand.KEY); + this.logger.verbose(`${slashCommands.length} SlashCommand (s) explored`); + + const db = await this.fetchGuildIds(); + for (const command of slashCommands) { + this.slashCommandService.remove(command.getName()); + const data = db.find(d => d.name === command.getName()); + if (!data) { + this.logger.warn(`No metadata found for SlashCommand : ${command.getName()}`); + this.slashCommandService.add(command); + return; + } + + this.logger.verbose(`Updating metadata for SlashCommand : ${command.getName()}`); + + command.setGuilds(data.guildIds ?? []); + this.slashCommandService.add(command); + } + } +} diff --git a/09-dynamic-guilds/src/dynamic.command.ts b/09-dynamic-guilds/src/dynamic.command.ts new file mode 100644 index 00000000..8661e3f3 --- /dev/null +++ b/09-dynamic-guilds/src/dynamic.command.ts @@ -0,0 +1,13 @@ +import { Injectable } from '@nestjs/common'; +import { Ctx, SlashCommand, SlashCommandContext } from 'necord'; + +@Injectable() +export class DynamicCommand { + @SlashCommand({ + name: 'dynamic', + description: 'This is a dynamic command' + }) + async run(@Ctx() [i]: SlashCommandContext) { + return i.reply({ ephemeral: true, content: 'I am so dynamic !! 😎' }); + } +} diff --git a/09-dynamic-guilds/src/main.ts b/09-dynamic-guilds/src/main.ts new file mode 100644 index 00000000..c1cd26f2 --- /dev/null +++ b/09-dynamic-guilds/src/main.ts @@ -0,0 +1,9 @@ +import { NestFactory } from '@nestjs/core'; +import { AppModule } from './app.module'; + +async function bootstrap() { + const app = await NestFactory.createApplicationContext(AppModule); + await app.init(); +} + +bootstrap(); diff --git a/09-dynamic-guilds/src/simple.command.ts b/09-dynamic-guilds/src/simple.command.ts new file mode 100644 index 00000000..08e8613f --- /dev/null +++ b/09-dynamic-guilds/src/simple.command.ts @@ -0,0 +1,10 @@ +import { Injectable } from '@nestjs/common'; +import { Ctx, SlashCommand, SlashCommandContext } from 'necord'; + +@Injectable() +export class SimpleCommand { + @SlashCommand({ name: 'ping', description: 'Bot status' }) + async run(@Ctx() [i]: SlashCommandContext) { + return i.reply({ ephemeral: true, content: 'Pong !' }); + } +} diff --git a/09-dynamic-guilds/tsconfig.build.json b/09-dynamic-guilds/tsconfig.build.json new file mode 100644 index 00000000..c151bb5d --- /dev/null +++ b/09-dynamic-guilds/tsconfig.build.json @@ -0,0 +1,5 @@ +{ + "extends": "./tsconfig.json", + "exclude": ["node_modules", "dist", "test", "**/*spec.ts"] +} + \ No newline at end of file diff --git a/09-dynamic-guilds/tsconfig.json b/09-dynamic-guilds/tsconfig.json new file mode 100644 index 00000000..1939a73a --- /dev/null +++ b/09-dynamic-guilds/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "removeComments": true, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "allowSyntheticDefaultImports": true, + "target": "ES2021", + "sourceMap": true, + "outDir": "./dist", + "baseUrl": "./", + "incremental": true, + "skipLibCheck": true + }, + "include": ["src/**/*"] +} + \ No newline at end of file