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.
- package/AUTHOR.md +2 -3
- package/README.md +361 -55
- package/dist/cjs/_shared/controller.decorator-MUHA_A3z.cjs +529 -0
- package/dist/cjs/core/index.cjs +246 -86
- package/dist/cjs/decorator/index.cjs +4 -2
- package/dist/cjs/enum/index.cjs +17 -4
- package/dist/cjs/testing/index.cjs +88 -4
- package/dist/esm/bin/app-template/README.md.template +61 -0
- package/dist/esm/bin/app-template/_env.example.template +2 -0
- package/dist/esm/bin/app-template/_gitignore.template +22 -0
- package/dist/esm/bin/app-template/_prettierrc.mjs.template +10 -0
- package/dist/esm/bin/app-template/eslint.config.ts.template +25 -0
- package/dist/esm/bin/app-template/meocord.config.ts.template +25 -0
- package/dist/esm/bin/app-template/package.json.template +38 -0
- package/dist/esm/bin/app-template/src/app.ts.template +44 -0
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.ts.template +19 -0
- package/dist/esm/bin/app-template/src/controllers/context-menu/builders/sample.builder.ts.template +10 -0
- package/dist/esm/bin/app-template/src/controllers/context-menu/sample.context-menu.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/context-menu/sample.context-menu.controller.ts.template +13 -0
- package/dist/esm/bin/app-template/src/controllers/message/sample.message.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/message/sample.message.controller.ts.template +28 -0
- package/dist/esm/bin/app-template/src/controllers/modal-submit/sample.modal-submit.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/modal-submit/sample.modal-submit.controller.ts.template +13 -0
- package/dist/esm/bin/app-template/src/controllers/reaction/sample.reaction.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/reaction/sample.reaction.controller.ts.template +29 -0
- package/dist/esm/bin/app-template/src/controllers/select-menu/sample.select-menu.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/select-menu/sample.select-menu.controller.ts.template +11 -0
- package/dist/esm/bin/app-template/src/controllers/slash/builders/sample.builder.ts.template +10 -0
- package/dist/esm/bin/app-template/src/controllers/slash/sample.slash.controller.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/controllers/slash/sample.slash.controller.ts.template +19 -0
- package/dist/esm/bin/app-template/src/guards/rate-limit.guard.spec.ts.template +13 -0
- package/dist/esm/bin/app-template/src/guards/rate-limit.guard.ts.template +52 -0
- package/dist/esm/bin/app-template/src/main.ts.template +14 -0
- package/dist/esm/bin/app-template/src/services/sample.service.spec.ts.template +17 -0
- package/dist/esm/bin/app-template/src/services/sample.service.ts.template +9 -0
- package/dist/esm/bin/app-template/tsconfig.eslint.json.template +5 -0
- package/dist/esm/bin/app-template/tsconfig.json.template +30 -0
- package/dist/esm/bin/app-template/tsconfig.test.json.template +8 -0
- package/dist/esm/bin/app-template/vitest.config.ts.template +35 -0
- package/dist/esm/bin/builder-template/builder/primary-entry-point.builder.template +26 -0
- package/dist/esm/bin/builder-template/controller/autocomplete.controller.template +16 -0
- package/dist/esm/bin/builder-template/controller/button.controller.template +1 -1
- package/dist/esm/bin/builder-template/controller/channel-select-menu.controller.template +12 -0
- package/dist/esm/bin/builder-template/controller/context-menu.controller.template +2 -2
- package/dist/esm/bin/builder-template/controller/mentionable-select-menu.controller.template +12 -0
- package/dist/esm/bin/builder-template/controller/modal-submit.controller.template +1 -1
- package/dist/esm/bin/builder-template/controller/primary-entry-point.controller.template +12 -0
- package/dist/esm/bin/builder-template/controller/role-select-menu.controller.template +12 -0
- package/dist/esm/bin/builder-template/controller/slash.controller.template +1 -1
- package/dist/esm/bin/builder-template/controller/user-select-menu.controller.template +12 -0
- package/dist/esm/bin/generator.js +4 -9
- package/dist/esm/bin/helper/app-generator.helper.js +78 -0
- package/dist/esm/bin/helper/controller-generator.helper.js +24 -7
- package/dist/esm/bin/meocord.js +156 -75
- package/dist/esm/core/meocord.app.js +247 -86
- package/dist/esm/decorator/controller.decorator.js +73 -10
- package/dist/esm/decorator/guard.decorator.js +1 -1
- package/dist/esm/decorator/index.js +1 -1
- package/dist/esm/enum/controller.enum.js +23 -4
- package/dist/esm/testing/mock-interaction.js +89 -5
- package/dist/esm/util/common.util.js +11 -3
- package/dist/esm/util/generator-cli.util.js +21 -4
- package/dist/esm/util/interaction.util.js +174 -0
- package/dist/esm/util/package-manager.util.js +9 -2
- package/dist/esm/util/package-version.util.js +32 -0
- package/dist/esm/util/runtime.util.js +72 -0
- package/dist/types/controller.enum-DYfhYaat.d.ts +36 -0
- package/dist/types/core/index.d.ts +75 -2
- package/dist/types/decorator/index.d.ts +61 -49
- package/dist/types/enum/index.d.ts +1 -1
- package/dist/types/interface/index.d.ts +89 -3
- package/dist/types/testing/index.d.ts +3 -21
- package/package.json +11 -12
- package/dist/cjs/_shared/controller.decorator-CC6BjHkS.cjs +0 -288
- 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,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,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 {}
|
package/dist/esm/bin/app-template/src/controllers/button/sample.button.controller.spec.ts.template
ADDED
|
@@ -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
|
+
}
|
package/dist/esm/bin/app-template/src/controllers/context-menu/builders/sample.builder.ts.template
ADDED
|
@@ -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
|
+
}
|
package/dist/esm/bin/app-template/src/controllers/message/sample.message.controller.spec.ts.template
ADDED
|
@@ -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
|
+
})
|
package/dist/esm/bin/app-template/src/controllers/message/sample.message.controller.ts.template
ADDED
|
@@ -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
|
+
})
|
package/dist/esm/bin/app-template/src/controllers/reaction/sample.reaction.controller.ts.template
ADDED
|
@@ -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
|
+
}
|
package/dist/esm/bin/app-template/src/controllers/slash/sample.slash.controller.spec.ts.template
ADDED
|
@@ -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
|
+
})
|