meocord 3.0.0 → 3.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. package/AUTHOR.md +2 -3
  2. package/README.md +361 -55
  3. package/dist/cjs/_shared/controller.decorator-MUHA_A3z.cjs +529 -0
  4. package/dist/cjs/core/index.cjs +246 -86
  5. package/dist/cjs/decorator/index.cjs +4 -2
  6. package/dist/cjs/enum/index.cjs +17 -4
  7. package/dist/cjs/testing/index.cjs +88 -4
  8. package/dist/esm/bin/app-template/README.md.template +61 -0
  9. package/dist/esm/bin/app-template/_env.example.template +2 -0
  10. package/dist/esm/bin/app-template/_gitignore.template +22 -0
  11. package/dist/esm/bin/app-template/_prettierrc.mjs.template +10 -0
  12. package/dist/esm/bin/app-template/eslint.config.ts.template +25 -0
  13. package/dist/esm/bin/app-template/meocord.config.ts.template +25 -0
  14. package/dist/esm/bin/app-template/package.json.template +38 -0
  15. package/dist/esm/bin/app-template/src/app.ts.template +44 -0
  16. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +17 -0
  17. package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +19 -0
  18. package/dist/esm/bin/app-template/src/controllers/context-menu/builders/sample.builder.ts.template +10 -0
  19. package/dist/esm/bin/app-template/src/controllers/context-menu/sample.context-menu.controller.spec.ts.template +17 -0
  20. package/dist/esm/bin/app-template/src/controllers/context-menu/sample.context-menu.controller.ts.template +13 -0
  21. package/dist/esm/bin/app-template/src/controllers/message/sample.message.controller.spec.ts.template +17 -0
  22. package/dist/esm/bin/app-template/src/controllers/message/sample.message.controller.ts.template +28 -0
  23. package/dist/esm/bin/app-template/src/controllers/modal-submit/sample.modal-submit.controller.spec.ts.template +17 -0
  24. package/dist/esm/bin/app-template/src/controllers/modal-submit/sample.modal-submit.controller.ts.template +13 -0
  25. package/dist/esm/bin/app-template/src/controllers/reaction/sample.reaction.controller.spec.ts.template +17 -0
  26. package/dist/esm/bin/app-template/src/controllers/reaction/sample.reaction.controller.ts.template +29 -0
  27. package/dist/esm/bin/app-template/src/controllers/select-menu/sample.select-menu.controller.spec.ts.template +17 -0
  28. package/dist/esm/bin/app-template/src/controllers/select-menu/sample.select-menu.controller.ts.template +11 -0
  29. package/dist/esm/bin/app-template/src/controllers/slash/builders/sample.builder.ts.template +10 -0
  30. package/dist/esm/bin/app-template/src/controllers/slash/sample.slash.controller.spec.ts.template +17 -0
  31. package/dist/esm/bin/app-template/src/controllers/slash/sample.slash.controller.ts.template +19 -0
  32. package/dist/esm/bin/app-template/src/guards/rate-limit.guard.spec.ts.template +13 -0
  33. package/dist/esm/bin/app-template/src/guards/rate-limit.guard.ts.template +52 -0
  34. package/dist/esm/bin/app-template/src/main.ts.template +14 -0
  35. package/dist/esm/bin/app-template/src/services/sample.service.spec.ts.template +17 -0
  36. package/dist/esm/bin/app-template/src/services/sample.service.ts.template +9 -0
  37. package/dist/esm/bin/app-template/tsconfig.eslint.json.template +5 -0
  38. package/dist/esm/bin/app-template/tsconfig.json.template +30 -0
  39. package/dist/esm/bin/app-template/tsconfig.test.json.template +8 -0
  40. package/dist/esm/bin/app-template/vitest.config.ts.template +35 -0
  41. package/dist/esm/bin/builder-template/builder/primary-entry-point.builder.template +26 -0
  42. package/dist/esm/bin/builder-template/controller/autocomplete.controller.template +16 -0
  43. package/dist/esm/bin/builder-template/controller/button.controller.template +1 -1
  44. package/dist/esm/bin/builder-template/controller/channel-select-menu.controller.template +12 -0
  45. package/dist/esm/bin/builder-template/controller/context-menu.controller.template +2 -2
  46. package/dist/esm/bin/builder-template/controller/mentionable-select-menu.controller.template +12 -0
  47. package/dist/esm/bin/builder-template/controller/modal-submit.controller.template +1 -1
  48. package/dist/esm/bin/builder-template/controller/primary-entry-point.controller.template +12 -0
  49. package/dist/esm/bin/builder-template/controller/role-select-menu.controller.template +12 -0
  50. package/dist/esm/bin/builder-template/controller/slash.controller.template +1 -1
  51. package/dist/esm/bin/builder-template/controller/user-select-menu.controller.template +12 -0
  52. package/dist/esm/bin/generator.js +4 -9
  53. package/dist/esm/bin/helper/app-generator.helper.js +78 -0
  54. package/dist/esm/bin/helper/controller-generator.helper.js +24 -7
  55. package/dist/esm/bin/meocord.js +156 -75
  56. package/dist/esm/core/meocord.app.js +247 -86
  57. package/dist/esm/decorator/controller.decorator.js +73 -10
  58. package/dist/esm/decorator/guard.decorator.js +1 -1
  59. package/dist/esm/decorator/index.js +1 -1
  60. package/dist/esm/enum/controller.enum.js +23 -4
  61. package/dist/esm/testing/mock-interaction.js +89 -5
  62. package/dist/esm/util/common.util.js +11 -3
  63. package/dist/esm/util/generator-cli.util.js +21 -4
  64. package/dist/esm/util/interaction.util.js +174 -0
  65. package/dist/esm/util/package-manager.util.js +9 -2
  66. package/dist/esm/util/package-version.util.js +32 -0
  67. package/dist/esm/util/runtime.util.js +72 -0
  68. package/dist/types/controller.enum-DYfhYaat.d.ts +36 -0
  69. package/dist/types/core/index.d.ts +75 -2
  70. package/dist/types/decorator/index.d.ts +61 -49
  71. package/dist/types/enum/index.d.ts +1 -1
  72. package/dist/types/interface/index.d.ts +89 -3
  73. package/dist/types/testing/index.d.ts +3 -21
  74. package/package.json +11 -12
  75. package/dist/cjs/_shared/controller.decorator-CC6BjHkS.cjs +0 -288
  76. package/dist/types/controller.enum-QA-IuReF.d.ts +0 -18
@@ -0,0 +1,61 @@
1
+ # {{displayName}}
2
+
3
+ A Discord bot built with [MeoCord](https://github.com/l7aromeo/meocord).
4
+
5
+ ## Setup
6
+
7
+ Copy the example environment file and add your bot token:
8
+
9
+ ```shell
10
+ cp .env.example .env
11
+ ```
12
+
13
+ `.env` is gitignored. Never commit a real token — if one is pushed, reset it in the
14
+ [Discord developer portal](https://discord.com/developers/applications) rather than
15
+ rewriting history.
16
+
17
+ ## Running
18
+
19
+ ```shell
20
+ {{packageManager}} run start:dev # watch mode, rebuilds and restarts on change
21
+ {{packageManager}} run start:prod # production
22
+ ```
23
+
24
+ The bot runs on whichever runtime you launch it with — launch with bun and it is a bun
25
+ process, launch with node and it is a node process.
26
+
27
+ ## Building
28
+
29
+ ```shell
30
+ {{packageManager}} run build:dev
31
+ {{packageManager}} run build:prod
32
+ ```
33
+
34
+ ## Generating components
35
+
36
+ ```shell
37
+ npx meocord g co slash "profile" # a slash controller, its spec and its builder
38
+ npx meocord g s "profile" # a service
39
+ npx meocord g gu "rate-limit" # a guard
40
+ npx meocord g --help # every generator
41
+ ```
42
+
43
+ Register whatever you generate in the `controllers` array in `src/app.ts` — controllers
44
+ are wired up by that list, not by where they sit on disk.
45
+
46
+ ## Testing
47
+
48
+ ```shell
49
+ {{packageManager}} run test
50
+ {{packageManager}} run test:coverage
51
+ ```
52
+
53
+ `meocord/testing` provides `MeoCordTestingModule` for building a controller with its
54
+ dependencies, plus `createMockInteraction` and friends for driving it without a Discord
55
+ connection.
56
+
57
+ ## Linting
58
+
59
+ ```shell
60
+ {{packageManager}} run lint
61
+ ```
@@ -0,0 +1,2 @@
1
+ # Copy to .env and fill in. Never commit the real token.
2
+ DISCORD_TOKEN=
@@ -0,0 +1,22 @@
1
+ dist
2
+ coverage
3
+
4
+ /node_modules
5
+ npm-debug.log*
6
+ yarn-debug.log*
7
+ yarn-error.log*
8
+
9
+ .pnp.*
10
+ .yarn/*
11
+ !.yarn/patches
12
+ !.yarn/plugins
13
+ !.yarn/releases
14
+ !.yarn/sdks
15
+ !.yarn/versions
16
+
17
+ .idea
18
+ .vscode/*
19
+
20
+ .env*
21
+ !.env.example
22
+ .DS_Store
@@ -0,0 +1,10 @@
1
+ export default {
2
+ bracketSpacing: true,
3
+ bracketSameLine: true,
4
+ singleQuote: true,
5
+ trailingComma: 'all',
6
+ arrowParens: 'avoid',
7
+ semi: false,
8
+ endOfLine: 'lf',
9
+ printWidth: 120,
10
+ }
@@ -0,0 +1,25 @@
1
+ import unusedImports from 'eslint-plugin-unused-imports'
2
+ import meocordESLint, { typescriptConfig } from 'meocord/eslint'
3
+
4
+ const customConfig = {
5
+ ...typescriptConfig,
6
+ plugins: {
7
+ ...typescriptConfig.plugins,
8
+ 'unused-imports': unusedImports,
9
+ },
10
+ rules: {
11
+ ...typescriptConfig.rules,
12
+ 'unused-imports/no-unused-imports': 'error',
13
+ 'unused-imports/no-unused-vars': [
14
+ 'error',
15
+ {
16
+ vars: 'all',
17
+ varsIgnorePattern: '^_',
18
+ args: 'after-used',
19
+ argsIgnorePattern: '^_',
20
+ },
21
+ ],
22
+ },
23
+ }
24
+
25
+ export default [...meocordESLint, customConfig]
@@ -0,0 +1,25 @@
1
+ import 'dotenv/config'
2
+ import { type MeoCordConfig } from 'meocord/interface'
3
+
4
+ export default {
5
+ appName: '{{displayName}}',
6
+ // Read from the environment rather than written here: this file is committed, and a
7
+ // token pasted into it is a token pushed to the remote. Copy .env.example to .env.
8
+ discordToken: process.env.DISCORD_TOKEN!,
9
+ webpack: config => {
10
+ config.module.rules?.push({
11
+ test: /\.(md|html)$/i,
12
+ type: 'asset/source',
13
+ })
14
+ config.module.rules?.push({
15
+ test: /\.(gif|jpg|jpeg|png|svg|woff|woff2|eot|ttf|otf)$/i,
16
+ type: 'asset/resource',
17
+ exclude: /node_modules/,
18
+ generator: {
19
+ filename: 'assets/[name][ext]',
20
+ },
21
+ })
22
+
23
+ return config
24
+ },
25
+ } satisfies MeoCordConfig
@@ -0,0 +1,38 @@
1
+ {
2
+ "name": "{{appName}}",
3
+ "version": "0.1.0",
4
+ "description": "",
5
+ "type": "module",
6
+ "private": true,
7
+ "scripts": {
8
+ "build:dev": "{{runtimePrefix}}meocord build --dev",
9
+ "build:prod": "{{runtimePrefix}}meocord build --prod",
10
+ "start:dev": "{{runtimePrefix}}meocord start --dev",
11
+ "start:prod": "{{runtimePrefix}}meocord start --prod",
12
+ "lint": "eslint --fix && tsc --noEmit && tsc --noEmit -p tsconfig.test.json",
13
+ "test": "vitest run",
14
+ "test:watch": "vitest",
15
+ "test:coverage": "vitest run --coverage"
16
+ },
17
+ "dependencies": {
18
+ "discord.js": "^14.27.0",
19
+ "dotenv": "^17.2.4",
20
+ "meocord": "^{{version}}"
21
+ },
22
+ "devDependencies": {
23
+ "@eslint/js": "^10.0.1",
24
+ "@typescript-eslint/parser": "^8.58.1",
25
+ "@vitest/coverage-istanbul": "^4.1.10",
26
+ "eslint": "^10.2.0",
27
+ "eslint-config-prettier": "^10.1.8",
28
+ "eslint-plugin-import-x": "^4.16.2",
29
+ "eslint-plugin-prettier": "^5.5.5",
30
+ "eslint-plugin-unused-imports": "^4.4.1",
31
+ "globals": "^17.5.0",
32
+ "prettier": "^3.8.2",
33
+ "typescript": "^6.0.2",
34
+ "typescript-eslint": "^8.58.1",
35
+ "unplugin-swc": "^1.5.7",
36
+ "vitest": "^4.1.10"
37
+ }
38
+ }
@@ -0,0 +1,44 @@
1
+ import { GatewayIntentBits, Partials, ActivityType } from 'discord.js'
2
+ import { MeoCord } from 'meocord/decorator'
3
+ import { SampleSlashController } from '@src/controllers/slash/sample.slash.controller'
4
+ import { SampleSelectMenuController } from '@src/controllers/select-menu/sample.select-menu.controller'
5
+ import { SampleButtonController } from '@src/controllers/button/sample.button.controller'
6
+ import { SampleMessageController } from '@src/controllers/message/sample.message.controller'
7
+ import { SampleReactionController } from '@src/controllers/reaction/sample.reaction.controller'
8
+ import { SampleContextMenuController } from '@src/controllers/context-menu/sample.context-menu.controller'
9
+ import { SampleModalSubmitController } from '@src/controllers/modal-submit/sample.modal-submit.controller'
10
+
11
+ @MeoCord({
12
+ controllers: [
13
+ // Slash Commands
14
+ SampleSlashController,
15
+ // Select Menu
16
+ SampleSelectMenuController,
17
+ // Buttons
18
+ SampleButtonController,
19
+ // Message
20
+ SampleMessageController,
21
+ // Reactions
22
+ SampleReactionController,
23
+ // Context Menu
24
+ SampleContextMenuController,
25
+ // Modal Submit
26
+ SampleModalSubmitController,
27
+ ],
28
+ clientOptions: {
29
+ intents: [
30
+ GatewayIntentBits.Guilds,
31
+ GatewayIntentBits.GuildMessages,
32
+ GatewayIntentBits.GuildMessageReactions,
33
+ GatewayIntentBits.MessageContent,
34
+ ],
35
+ partials: [Partials.Message, Partials.Reaction],
36
+ },
37
+ activities: [
38
+ {
39
+ name: ``,
40
+ type: ActivityType.Custom,
41
+ },
42
+ ],
43
+ })
44
+ export default class App {}
@@ -0,0 +1,17 @@
1
+ import { MeoCordTestingModule } from 'meocord/testing'
2
+ import { SampleButtonController } from '@src/controllers/button/sample.button.controller.js'
3
+
4
+ describe('SampleButtonController', () => {
5
+ let controller: SampleButtonController
6
+
7
+ beforeEach(() => {
8
+ const module = MeoCordTestingModule.create({
9
+ controllers: [SampleButtonController],
10
+ }).compile()
11
+ controller = module.get(SampleButtonController)
12
+ })
13
+
14
+ it('should be defined', () => {
15
+ expect(controller).toBeDefined()
16
+ })
17
+ })
@@ -0,0 +1,19 @@
1
+ import { ButtonInteraction } from 'discord.js'
2
+ import { Controller, Command, UseGuard } from 'meocord/decorator'
3
+ import { CommandType } from 'meocord/enum'
4
+ import { RateLimitGuard } from '@src/guards/rate-limit.guard'
5
+
6
+ @Controller()
7
+ export class SampleButtonController {
8
+ @Command('button-click', CommandType.BUTTON)
9
+ @UseGuard(RateLimitGuard)
10
+ async handleButton(interaction: ButtonInteraction) {
11
+ await interaction.reply('Button clicked!')
12
+ }
13
+
14
+ @Command('button-with/{id}', CommandType.BUTTON)
15
+ @UseGuard({ provide: RateLimitGuard, params: { limit: 2, windowInSeconds: 60 } })
16
+ async handleButtonWithId(interaction: ButtonInteraction, { id }: { id: string }) {
17
+ await interaction.reply(`Button with id: ${id} clicked!`)
18
+ }
19
+ }
@@ -0,0 +1,10 @@
1
+ import { ContextMenuCommandBuilder, ApplicationCommandType } from 'discord.js'
2
+ import { CommandBuilder } from 'meocord/decorator'
3
+ import { CommandType } from 'meocord/enum'
4
+
5
+ @CommandBuilder(CommandType.CONTEXT_MENU)
6
+ export class SampleCommandBuilder {
7
+ build() {
8
+ return new ContextMenuCommandBuilder().setName('sample-context-menu').setType(ApplicationCommandType.User)
9
+ }
10
+ }
@@ -0,0 +1,17 @@
1
+ import { MeoCordTestingModule } from 'meocord/testing'
2
+ import { SampleContextMenuController } from '@src/controllers/context-menu/sample.context-menu.controller.js'
3
+
4
+ describe('SampleContextMenuController', () => {
5
+ let controller: SampleContextMenuController
6
+
7
+ beforeEach(() => {
8
+ const module = MeoCordTestingModule.create({
9
+ controllers: [SampleContextMenuController],
10
+ }).compile()
11
+ controller = module.get(SampleContextMenuController)
12
+ })
13
+
14
+ it('should be defined', () => {
15
+ expect(controller).toBeDefined()
16
+ })
17
+ })
@@ -0,0 +1,13 @@
1
+ import { UserContextMenuCommandInteraction, MessageContextMenuCommandInteraction } from 'discord.js'
2
+ import { Controller, Command, UseGuard } from 'meocord/decorator'
3
+ import { SampleCommandBuilder } from '@src/controllers/context-menu/builders/sample.builder'
4
+ import { RateLimitGuard } from '@src/guards/rate-limit.guard'
5
+
6
+ @Controller()
7
+ export class SampleContextMenuController {
8
+ @Command('sample-context-menu', SampleCommandBuilder)
9
+ @UseGuard(RateLimitGuard)
10
+ async handleContextMenu(interaction: UserContextMenuCommandInteraction | MessageContextMenuCommandInteraction) {
11
+ await interaction.reply('This is sample reply of context menu command.')
12
+ }
13
+ }
@@ -0,0 +1,17 @@
1
+ import { MeoCordTestingModule } from 'meocord/testing'
2
+ import { SampleMessageController } from '@src/controllers/message/sample.message.controller.js'
3
+
4
+ describe('SampleMessageController', () => {
5
+ let controller: SampleMessageController
6
+
7
+ beforeEach(() => {
8
+ const module = MeoCordTestingModule.create({
9
+ controllers: [SampleMessageController],
10
+ }).compile()
11
+ controller = module.get(SampleMessageController)
12
+ })
13
+
14
+ it('should be defined', () => {
15
+ expect(controller).toBeDefined()
16
+ })
17
+ })
@@ -0,0 +1,28 @@
1
+ import { MessageHandler, Controller } from 'meocord/decorator'
2
+ import { Message } from 'discord.js'
3
+ import { Logger } from 'meocord/common'
4
+
5
+ @Controller()
6
+ export class SampleMessageController {
7
+ private readonly logger = new Logger(SampleMessageController.name)
8
+
9
+ @MessageHandler('baka')
10
+ @MessageHandler('Baka')
11
+ async baka(message: Message): Promise<void> {
12
+ try {
13
+ await message.reply(`You Baka!`)
14
+ } catch (error) {
15
+ this.logger.error('Error replying to command', error)
16
+ }
17
+ }
18
+
19
+ @MessageHandler()
20
+ async handleGeneralMessage(message: Message): Promise<void> {
21
+ try {
22
+ this.logger.info(`Received message: ${message.content}`)
23
+ // TODO: Implements your message handler logic here
24
+ } catch (error) {
25
+ this.logger.error('Error handling the message', error)
26
+ }
27
+ }
28
+ }
@@ -0,0 +1,17 @@
1
+ import { MeoCordTestingModule } from 'meocord/testing'
2
+ import { SampleModalSubmitController } from '@src/controllers/modal-submit/sample.modal-submit.controller.js'
3
+
4
+ describe('SampleModalSubmitController', () => {
5
+ let controller: SampleModalSubmitController
6
+
7
+ beforeEach(() => {
8
+ const module = MeoCordTestingModule.create({
9
+ controllers: [SampleModalSubmitController],
10
+ }).compile()
11
+ controller = module.get(SampleModalSubmitController)
12
+ })
13
+
14
+ it('should be defined', () => {
15
+ expect(controller).toBeDefined()
16
+ })
17
+ })
@@ -0,0 +1,13 @@
1
+ import { ModalSubmitInteraction } from 'discord.js'
2
+ import { Controller, Command, UseGuard } from 'meocord/decorator'
3
+ import { CommandType } from 'meocord/enum'
4
+ import { RateLimitGuard } from '@src/guards/rate-limit.guard'
5
+
6
+ @Controller()
7
+ export class SampleModalSubmitController {
8
+ @Command('submit-modal', CommandType.MODAL_SUBMIT)
9
+ @UseGuard(RateLimitGuard)
10
+ async handleModal(interaction: ModalSubmitInteraction) {
11
+ await interaction.reply('Modal submitted!')
12
+ }
13
+ }
@@ -0,0 +1,17 @@
1
+ import { MeoCordTestingModule } from 'meocord/testing'
2
+ import { SampleReactionController } from '@src/controllers/reaction/sample.reaction.controller.js'
3
+
4
+ describe('SampleReactionController', () => {
5
+ let controller: SampleReactionController
6
+
7
+ beforeEach(() => {
8
+ const module = MeoCordTestingModule.create({
9
+ controllers: [SampleReactionController],
10
+ }).compile()
11
+ controller = module.get(SampleReactionController)
12
+ })
13
+
14
+ it('should be defined', () => {
15
+ expect(controller).toBeDefined()
16
+ })
17
+ })
@@ -0,0 +1,29 @@
1
+ import { MessageReaction } from 'discord.js'
2
+ import { Controller, ReactionHandler } from 'meocord/decorator'
3
+ import { Logger } from 'meocord/common'
4
+ import { ReactionHandlerAction } from 'meocord/enum'
5
+ import { type ReactionHandlerOptions } from 'meocord/interface'
6
+
7
+ @Controller()
8
+ export class SampleReactionController {
9
+ private readonly logger = new Logger(SampleReactionController.name)
10
+
11
+ @ReactionHandler('😋')
12
+ async handleReaction(reaction: MessageReaction, { user, action }: ReactionHandlerOptions) {
13
+ this.logger.log(`Reaction 😋 ${action === ReactionHandlerAction.ADD ? 'added' : 'removed'}.`)
14
+
15
+ if (!reaction.message) return
16
+
17
+ if (action === ReactionHandlerAction.ADD) {
18
+ await reaction.message.reply(`${user.username} reacted with 😋!`)
19
+ }
20
+ }
21
+
22
+ @ReactionHandler()
23
+ async handleAnyReaction(reaction: MessageReaction, { user, action }: ReactionHandlerOptions) {
24
+ this.logger.log('Reaction detected!')
25
+ if (reaction.message && action === ReactionHandlerAction.ADD) {
26
+ await reaction.message.reply(`${user.username} reacted with ${reaction.emoji.name}!`)
27
+ }
28
+ }
29
+ }
@@ -0,0 +1,17 @@
1
+ import { MeoCordTestingModule } from 'meocord/testing'
2
+ import { SampleSelectMenuController } from '@src/controllers/select-menu/sample.select-menu.controller.js'
3
+
4
+ describe('SampleSelectMenuController', () => {
5
+ let controller: SampleSelectMenuController
6
+
7
+ beforeEach(() => {
8
+ const module = MeoCordTestingModule.create({
9
+ controllers: [SampleSelectMenuController],
10
+ }).compile()
11
+ controller = module.get(SampleSelectMenuController)
12
+ })
13
+
14
+ it('should be defined', () => {
15
+ expect(controller).toBeDefined()
16
+ })
17
+ })
@@ -0,0 +1,11 @@
1
+ import { StringSelectMenuInteraction } from 'discord.js'
2
+ import { Controller, Command } from 'meocord/decorator'
3
+ import { CommandType } from 'meocord/enum'
4
+
5
+ @Controller()
6
+ export class SampleSelectMenuController {
7
+ @Command('select-menu', CommandType.SELECT_MENU)
8
+ async handleSelectMenu(interaction: StringSelectMenuInteraction) {
9
+ await interaction.reply('Select menu used!')
10
+ }
11
+ }
@@ -0,0 +1,10 @@
1
+ import { SlashCommandBuilder } from 'discord.js'
2
+ import { CommandBuilder } from 'meocord/decorator'
3
+ import { CommandType } from 'meocord/enum'
4
+
5
+ @CommandBuilder(CommandType.SLASH)
6
+ export class SampleCommandBuilder {
7
+ build(commandName: string) {
8
+ return new SlashCommandBuilder().setName(commandName).setDescription('This is a sample slash command')
9
+ }
10
+ }
@@ -0,0 +1,17 @@
1
+ import { MeoCordTestingModule } from 'meocord/testing'
2
+ import { SampleSlashController } from '@src/controllers/slash/sample.slash.controller.js'
3
+
4
+ describe('SampleSlashController', () => {
5
+ let controller: SampleSlashController
6
+
7
+ beforeEach(() => {
8
+ const module = MeoCordTestingModule.create({
9
+ controllers: [SampleSlashController],
10
+ }).compile()
11
+ controller = module.get(SampleSlashController)
12
+ })
13
+
14
+ it('should be defined', () => {
15
+ expect(controller).toBeDefined()
16
+ })
17
+ })
@@ -0,0 +1,19 @@
1
+ import { ChatInputCommandInteraction } from 'discord.js'
2
+ import { Controller, Command, UseGuard } from 'meocord/decorator'
3
+ import { SampleCommandBuilder } from '@src/controllers/slash/builders/sample.builder'
4
+ import { RateLimitGuard } from '@src/guards/rate-limit.guard'
5
+
6
+ @Controller()
7
+ export class SampleSlashController {
8
+ @Command('sample-slash', SampleCommandBuilder)
9
+ @UseGuard(RateLimitGuard)
10
+ async handleSampleSlash(interaction: ChatInputCommandInteraction) {
11
+ await interaction.reply('This is sample reply of slash command.')
12
+ }
13
+
14
+ @Command('sample-slash-2', SampleCommandBuilder)
15
+ @UseGuard({ provide: RateLimitGuard, params: { limit: 2, windowInSeconds: 60 } })
16
+ async handleSampleSlashTwo(interaction: ChatInputCommandInteraction) {
17
+ await interaction.reply('This is sample reply of slash command.')
18
+ }
19
+ }
@@ -0,0 +1,13 @@
1
+ import { RateLimitGuard } from '@src/guards/rate-limit.guard.js'
2
+
3
+ describe('RateLimitGuard', () => {
4
+ let guard: RateLimitGuard
5
+
6
+ beforeEach(() => {
7
+ guard = new RateLimitGuard({ limit: 3, windowInSeconds: 60 })
8
+ })
9
+
10
+ it('should be defined', () => {
11
+ expect(guard).toBeDefined()
12
+ })
13
+ })
@@ -0,0 +1,52 @@
1
+ import { Guard } from 'meocord/decorator'
2
+ import { type GuardInterface } from 'meocord/interface'
3
+ import { BaseInteraction } from 'discord.js'
4
+ import { Logger } from 'meocord/common'
5
+
6
+ class RateLimiterGuardOptions {
7
+ limit?: number
8
+ windowInSeconds?: number
9
+ }
10
+
11
+ @Guard()
12
+ export class RateLimitGuard implements GuardInterface {
13
+ // ==============================
14
+ // NOTE: Consider using Redis or another distributed store for rate limits in production
15
+ // ==============================
16
+ private logger = new Logger(RateLimitGuard.name)
17
+
18
+ private rateLimits = new Map<string, number[]>()
19
+ private readonly limit: number
20
+ private readonly windowInSeconds: number
21
+
22
+ constructor(options: RateLimiterGuardOptions = {}) {
23
+ this.limit = options.limit || 5
24
+ this.windowInSeconds = options.windowInSeconds || 60
25
+ }
26
+
27
+ async canActivate(interaction: BaseInteraction): Promise<boolean> {
28
+ const userId = interaction.user.id
29
+
30
+ let key: string
31
+ if (interaction.isChatInputCommand()) {
32
+ const commandId = interaction.commandName
33
+ const subCommand = interaction.options.getSubcommand(false) || ''
34
+ key = subCommand ? `${commandId}:${subCommand}:${userId}` : `${commandId}:${userId}`
35
+ } else if (interaction.isMessageComponent()) {
36
+ key = `${interaction.customId}:${userId}`
37
+ } else {
38
+ key = userId
39
+ }
40
+
41
+ const currentTime = Math.floor(Date.now() / 1000)
42
+ const timestamps = this.rateLimits.get(key) || []
43
+
44
+ const filteredTimestamps = timestamps.filter(time => currentTime - time < this.windowInSeconds)
45
+ filteredTimestamps.push(currentTime)
46
+ this.rateLimits.set(key, filteredTimestamps)
47
+
48
+ this.logger.debug(key, filteredTimestamps.length, this.limit, this.windowInSeconds)
49
+
50
+ return filteredTimestamps.length <= this.limit
51
+ }
52
+ }
@@ -0,0 +1,14 @@
1
+ import App from '@src/app'
2
+ import { Logger } from 'meocord/common'
3
+ import { MeoCordFactory } from 'meocord/core'
4
+
5
+ const logger = new Logger()
6
+ const app = MeoCordFactory.create(App)
7
+
8
+ async function bootstrap() {
9
+ logger.log('Starting application')
10
+ await app.start()
11
+ logger.log('Application started')
12
+ }
13
+
14
+ bootstrap().catch(error => logger.error('Error during startup:', error))
@@ -0,0 +1,17 @@
1
+ import { MeoCordTestingModule } from 'meocord/testing'
2
+ import { SampleService } from '@src/services/sample.service.js'
3
+
4
+ describe('SampleService', () => {
5
+ let service: SampleService
6
+
7
+ beforeEach(() => {
8
+ const module = MeoCordTestingModule.create({
9
+ providers: [{ provide: SampleService, useClass: SampleService }],
10
+ }).compile()
11
+ service = module.get(SampleService)
12
+ })
13
+
14
+ it('should be defined', () => {
15
+ expect(service).toBeDefined()
16
+ })
17
+ })
@@ -0,0 +1,9 @@
1
+ import { Service } from 'meocord/decorator'
2
+
3
+ @Service()
4
+ export class SampleService {
5
+ async handleInteraction() {
6
+ // TODO: Implement service logic
7
+ return 'SampleService is working!'
8
+ }
9
+ }
@@ -0,0 +1,5 @@
1
+ {
2
+ "extends": "./tsconfig.json",
3
+ "include": ["meocord.eslint.ts", "meocord.config.ts", "vitest.config.ts", "eslint.config.ts"],
4
+ "exclude": ["node_modules"]
5
+ }