meocord 4.0.0-beta.1 → 4.0.0-beta.2

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/CHANGELOG.md CHANGED
@@ -1,5 +1,30 @@
1
1
  # meocord
2
2
 
3
+ ## 4.0.0-beta.2
4
+
5
+ ### Major Changes
6
+
7
+ - [#36](https://github.com/l7aromeo/meocord/pull/36) [`45e564e`](https://github.com/l7aromeo/meocord/commit/45e564e6baff17313037372ec56baa1711a3584c) Thanks [@l7aromeo](https://github.com/l7aromeo)! - **Breaking:** `app.start()` rejects when the login fails. It logged the error and resolved, so a bot
8
+ with an invalid token went on to log "Application started" and exit with code 0, which Docker's
9
+ `restart: on-failure`, systemd and CI all read as success. New applications' `main.ts` sets
10
+ `process.exitCode = 1` when startup fails; add the same to an existing entry point's `catch`. See the
11
+ [migration guide](https://github.com/l7aromeo/meocord/blob/main/docs/MIGRATING.md#4-build-and-start).
12
+
13
+ ### Patch Changes
14
+
15
+ - [#37](https://github.com/l7aromeo/meocord/pull/37) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Type the CommonJS build as CommonJS. Every entry point's `require` condition resolved to the ESM
16
+ declarations, so a CommonJS TypeScript project was told `meocord/core` is an ES module it cannot
17
+ `require`, even with `skipLibCheck`. Each entry now ships `.d.cts` declarations for `require`, and
18
+ `meocord/eslint` types its `module.exports` array as what `require` returns.
19
+
20
+ - [#35](https://github.com/l7aromeo/meocord/pull/35) [`b93a771`](https://github.com/l7aromeo/meocord/commit/b93a771b19664345fec4222d5a2c29dcf5b31a0d) Thanks [@l7aromeo](https://github.com/l7aromeo)! - Generate code that passes a new application's own `lint`. A generated guard failed `tsc` and ESLint
21
+ — `GuardInterface` imported as a value, and an unused `context` parameter — and generated message and
22
+ reaction controllers imported names they never used, which only the application's ESLint, run in the
23
+ background after generating, removed. New applications also typecheck `meocord.config.ts`: the
24
+ template's `tsconfig.json` includes it instead of excluding it, so a type error in the config fails
25
+ `lint`, and editors resolve `paths` aliases imported there. `noEmit` stays on, so `tsc` writes nothing
26
+ beside it.
27
+
3
28
  ## 4.0.0-beta.1
4
29
 
5
30
  ### Major Changes
@@ -106,28 +106,30 @@ class MeoCordApp {
106
106
  }
107
107
  return this.controllerInstancesCache.get(controllerClass);
108
108
  }
109
- async start() {
110
- try {
111
- this.logger.log('Starting bot...');
112
- this.bot.on('clientReady', ()=>this.runListener('clientReady', async ()=>{
113
- this.activityInterval = setInterval(()=>this.updateActivity(), 10000);
114
- await this.registerCommands();
115
- }));
116
- this.bot.on('interactionCreate', (interaction)=>this.runListener('interactionCreate', ()=>this.handleInteraction(interaction)));
117
- this.bot.on('messageCreate', (message)=>this.runListener('messageCreate', ()=>this.handleMessage(message)));
118
- this.bot.on('messageReactionAdd', (reaction, user)=>this.runListener('messageReactionAdd', ()=>this.handleReaction(reaction, {
119
- user,
120
- action: enum_index.ReactionHandlerAction.ADD
121
- })));
122
- this.bot.on('messageReactionRemove', (reaction, user)=>this.runListener('messageReactionRemove', ()=>this.handleReaction(reaction, {
123
- user,
124
- action: enum_index.ReactionHandlerAction.REMOVE
125
- })));
126
- await this.bot.login(this.discordToken);
127
- this.logger.log('Bot is online!');
128
- } catch (error) {
129
- this.logger.error('Error during bot startup:', error);
130
- }
109
+ /**
110
+ * Registers the Discord event handlers and logs in.
111
+ *
112
+ * Rejects when the login fails -- an invalid token, or Discord unreachable -- so the caller can
113
+ * stop with a non-zero exit code. A bot that never came online is a failed start, and a
114
+ * supervisor such as Docker's `restart: on-failure` or systemd can only tell if the process says so.
115
+ */ async start() {
116
+ this.logger.log('Starting bot...');
117
+ this.bot.on('clientReady', ()=>this.runListener('clientReady', async ()=>{
118
+ this.activityInterval = setInterval(()=>this.updateActivity(), 10000);
119
+ await this.registerCommands();
120
+ }));
121
+ this.bot.on('interactionCreate', (interaction)=>this.runListener('interactionCreate', ()=>this.handleInteraction(interaction)));
122
+ this.bot.on('messageCreate', (message)=>this.runListener('messageCreate', ()=>this.handleMessage(message)));
123
+ this.bot.on('messageReactionAdd', (reaction, user)=>this.runListener('messageReactionAdd', ()=>this.handleReaction(reaction, {
124
+ user,
125
+ action: enum_index.ReactionHandlerAction.ADD
126
+ })));
127
+ this.bot.on('messageReactionRemove', (reaction, user)=>this.runListener('messageReactionRemove', ()=>this.handleReaction(reaction, {
128
+ user,
129
+ action: enum_index.ReactionHandlerAction.REMOVE
130
+ })));
131
+ await this.bot.login(this.discordToken);
132
+ this.logger.log('Bot is online!');
131
133
  }
132
134
  async registerCommands() {
133
135
  // Keyed by type and name together, because that pair is what Discord treats as one
@@ -11,4 +11,8 @@ async function bootstrap() {
11
11
  logger.log('Application started')
12
12
  }
13
13
 
14
- bootstrap().catch(error => logger.error('Error during startup:', error))
14
+ bootstrap().catch(error => {
15
+ logger.error('Error during startup:', error)
16
+ // A bot that never came online is a failed start: exit non-zero, so Docker, systemd or CI see it.
17
+ process.exitCode = 1
18
+ })
@@ -25,6 +25,6 @@
25
25
  },
26
26
  "types": ["node"]
27
27
  },
28
- "include": ["src/**/*.ts"],
29
- "exclude": ["meocord.config.ts", "dist", "vitest.config.ts", "node_modules", "src/**/*.spec.ts"]
28
+ "include": ["src/**/*.ts", "meocord.config.ts"],
29
+ "exclude": ["dist", "vitest.config.ts", "node_modules", "src/**/*.spec.ts"]
30
30
  }
@@ -1,5 +1,5 @@
1
- import { MessageHandler, Controller, Command } from 'meocord/decorator'
2
- import { EmbedBuilder, Message } from 'discord.js'
1
+ import { MessageHandler, Controller } from 'meocord/decorator'
2
+ import { Message } from 'discord.js'
3
3
  import { Logger } from 'meocord/common'
4
4
 
5
5
  @Controller()
@@ -1,7 +1,7 @@
1
- import { MessageReaction, User } from 'discord.js'
1
+ import { MessageReaction } from 'discord.js'
2
2
  import { Controller, ReactionHandler } from 'meocord/decorator'
3
3
  import { Logger } from 'meocord/common'
4
- import { CommandType, ReactionHandlerAction } from 'meocord/enum'
4
+ import { ReactionHandlerAction } from 'meocord/enum'
5
5
  import { type ReactionHandlerOptions } from 'meocord/interface'
6
6
 
7
7
  @Controller()
@@ -10,12 +10,12 @@ export class {{className}}ReactionController {
10
10
 
11
11
  @ReactionHandler('😋')
12
12
  async handleReaction(reaction: MessageReaction, { user, action }: ReactionHandlerOptions) {
13
- this.logger.log(`Reaction 😋 ${action === ReactionHandlerAction.ADD ? 'added' : 'removed'}.`);
13
+ this.logger.log(`Reaction 😋 ${action === ReactionHandlerAction.ADD ? 'added' : 'removed'}.`)
14
14
 
15
- if (!reaction.message) return;
15
+ if (!reaction.message) return
16
16
 
17
17
  if (action === ReactionHandlerAction.ADD) {
18
- await reaction.message.reply(`${user.username} reacted with 😋!`);
18
+ await reaction.message.reply(`${user.username} reacted with 😋!`)
19
19
  }
20
20
  }
21
21
 
@@ -1,10 +1,10 @@
1
1
  import { Guard } from 'meocord/decorator'
2
- import { GuardInterface } from 'meocord/interface'
2
+ import { type GuardInterface } from 'meocord/interface'
3
3
  import { BaseInteraction, Message, MessageReaction } from 'discord.js'
4
4
 
5
5
  @Guard()
6
6
  export class {{className}}Guard implements GuardInterface {
7
- async canActivate(context: BaseInteraction | Message | MessageReaction): Promise<boolean> {
7
+ async canActivate(_context: BaseInteraction | Message | MessageReaction): Promise<boolean> {
8
8
  // TODO: Implement the guard logic to determine if the interaction is allowed
9
9
  return true
10
10
  }
@@ -88,28 +88,30 @@ class MeoCordApp {
88
88
  }
89
89
  return this.controllerInstancesCache.get(controllerClass);
90
90
  }
91
- async start() {
92
- try {
93
- this.logger.log('Starting bot...');
94
- this.bot.on('clientReady', ()=>this.runListener('clientReady', async ()=>{
95
- this.activityInterval = setInterval(()=>this.updateActivity(), 10000);
96
- await this.registerCommands();
97
- }));
98
- this.bot.on('interactionCreate', (interaction)=>this.runListener('interactionCreate', ()=>this.handleInteraction(interaction)));
99
- this.bot.on('messageCreate', (message)=>this.runListener('messageCreate', ()=>this.handleMessage(message)));
100
- this.bot.on('messageReactionAdd', (reaction, user)=>this.runListener('messageReactionAdd', ()=>this.handleReaction(reaction, {
101
- user,
102
- action: ReactionHandlerAction.ADD
103
- })));
104
- this.bot.on('messageReactionRemove', (reaction, user)=>this.runListener('messageReactionRemove', ()=>this.handleReaction(reaction, {
105
- user,
106
- action: ReactionHandlerAction.REMOVE
107
- })));
108
- await this.bot.login(this.discordToken);
109
- this.logger.log('Bot is online!');
110
- } catch (error) {
111
- this.logger.error('Error during bot startup:', error);
112
- }
91
+ /**
92
+ * Registers the Discord event handlers and logs in.
93
+ *
94
+ * Rejects when the login fails -- an invalid token, or Discord unreachable -- so the caller can
95
+ * stop with a non-zero exit code. A bot that never came online is a failed start, and a
96
+ * supervisor such as Docker's `restart: on-failure` or systemd can only tell if the process says so.
97
+ */ async start() {
98
+ this.logger.log('Starting bot...');
99
+ this.bot.on('clientReady', ()=>this.runListener('clientReady', async ()=>{
100
+ this.activityInterval = setInterval(()=>this.updateActivity(), 10000);
101
+ await this.registerCommands();
102
+ }));
103
+ this.bot.on('interactionCreate', (interaction)=>this.runListener('interactionCreate', ()=>this.handleInteraction(interaction)));
104
+ this.bot.on('messageCreate', (message)=>this.runListener('messageCreate', ()=>this.handleMessage(message)));
105
+ this.bot.on('messageReactionAdd', (reaction, user)=>this.runListener('messageReactionAdd', ()=>this.handleReaction(reaction, {
106
+ user,
107
+ action: ReactionHandlerAction.ADD
108
+ })));
109
+ this.bot.on('messageReactionRemove', (reaction, user)=>this.runListener('messageReactionRemove', ()=>this.handleReaction(reaction, {
110
+ user,
111
+ action: ReactionHandlerAction.REMOVE
112
+ })));
113
+ await this.bot.login(this.discordToken);
114
+ this.logger.log('Bot is online!');
113
115
  }
114
116
  async registerCommands() {
115
117
  // Keyed by type and name together, because that pair is what Discord treats as one
@@ -1,4 +1,4 @@
1
- var version = "4.0.0-beta.1";
1
+ var version = "4.0.0-beta.2";
2
2
  var packageJson = {
3
3
  version: version};
4
4
 
@@ -0,0 +1,53 @@
1
+ import { ColorResolvable } from 'discord.js';
2
+
3
+ declare class Logger {
4
+ private context?;
5
+ private readonly colorMap;
6
+ constructor(context?: string | undefined);
7
+ log(...args: any[]): void;
8
+ info(...args: any[]): void;
9
+ warn(...args: any[]): void;
10
+ error(...args: any[]): void;
11
+ debug(...args: any[]): void;
12
+ verbose(...args: any[]): void;
13
+ private formatMessage;
14
+ private logWithContext;
15
+ }
16
+
17
+ declare class Theme {
18
+ static successColor: ColorResolvable;
19
+ static infoColor: ColorResolvable;
20
+ static errorColor: ColorResolvable;
21
+ static warningColor: ColorResolvable;
22
+ }
23
+
24
+ /**
25
+ * Composes multiple class or method decorators into a single decorator.
26
+ *
27
+ * @example
28
+ * ```typescript
29
+ * export const Protected = () => applyDecorators(
30
+ * UseGuard(DefaultGuard, GlobalRateLimiterGuard),
31
+ * )
32
+ *
33
+ * @Controller()
34
+ * @Protected()
35
+ * export class PingController {}
36
+ * ```
37
+ */
38
+ declare function applyDecorators(...decorators: (ClassDecorator | MethodDecorator)[]): ClassDecorator & MethodDecorator;
39
+ /**
40
+ * Attaches arbitrary metadata to a class or method. Use alongside `Reflect.getMetadata` to read it back.
41
+ *
42
+ * @example
43
+ * ```typescript
44
+ * export const Roles = (...roles: string[]) => SetMetadata('roles', roles)
45
+ *
46
+ * @Command('admin', CommandType.SLASH)
47
+ * @Roles('admin', 'moderator')
48
+ * async adminCommand(interaction: ChatInputCommandInteraction) {}
49
+ * ```
50
+ */
51
+ declare function SetMetadata<V = any>(metadataKey: string, metadataValue: V): ClassDecorator & MethodDecorator;
52
+
53
+ export { Logger, SetMetadata, Theme, applyDecorators };
@@ -0,0 +1,36 @@
1
+ /**
2
+ * The kinds of interaction a `@Command` method can be bound to.
3
+ *
4
+ * Each member names one Discord interaction shape rather than a family of them, so a
5
+ * handler's parameter type follows from its command type alone. That is why the four
6
+ * entity select menus are separate members instead of one `SELECT_MENU`: Discord sends
7
+ * them as distinct component types (5-8) carrying different resolved data, and
8
+ * collapsing them would leave the handler with a union it has to re-narrow by hand.
9
+ */
10
+ declare enum CommandType {
11
+ /** Chat input command, or one subcommand of it. */
12
+ SLASH = "SLASH",
13
+ /** User or message context menu command. */
14
+ CONTEXT_MENU = "CONTEXT_MENU",
15
+ /** Activity launch command (`ApplicationCommandType.PrimaryEntryPoint`). */
16
+ PRIMARY_ENTRY_POINT = "PRIMARY_ENTRY_POINT",
17
+ BUTTON = "BUTTON",
18
+ /** String select menu — the one whose options the application defines itself. */
19
+ SELECT_MENU = "SELECT_MENU",
20
+ USER_SELECT_MENU = "USER_SELECT_MENU",
21
+ ROLE_SELECT_MENU = "ROLE_SELECT_MENU",
22
+ MENTIONABLE_SELECT_MENU = "MENTIONABLE_SELECT_MENU",
23
+ CHANNEL_SELECT_MENU = "CHANNEL_SELECT_MENU",
24
+ MODAL_SUBMIT = "MODAL_SUBMIT"
25
+ }
26
+ /**
27
+ * Enum representing actions that can be performed on a message reaction.
28
+ */
29
+ declare enum ReactionHandlerAction {
30
+ /** Reaction added to a message. */
31
+ ADD = "ADD",
32
+ /** Reaction removed from a message. */
33
+ REMOVE = "REMOVE"
34
+ }
35
+
36
+ export { CommandType as C, ReactionHandlerAction as R };
@@ -0,0 +1,131 @@
1
+ import { Container, ServiceIdentifier } from 'inversify';
2
+ import { Client, ActivityOptions } from 'discord.js';
3
+
4
+ declare class MeoCordApp {
5
+ private readonly controllerClasses;
6
+ private readonly container;
7
+ private readonly discordClient;
8
+ private discordToken;
9
+ private activities?;
10
+ private readonly logger;
11
+ private readonly bot;
12
+ private isShuttingDown;
13
+ private activityInterval;
14
+ private controllerInstancesCache;
15
+ constructor(controllerClasses: (new (...args: any[]) => any)[], container: Container, discordClient: Client, discordToken: string, activities?: ActivityOptions[] | undefined);
16
+ /**
17
+ * Runs an event handler so a failure inside it cannot take the process down.
18
+ *
19
+ * discord.js calls listeners without awaiting them, so a rejection escaping one has
20
+ * nothing left to settle it: Node reports an unhandled rejection, which terminates
21
+ * the process by default. Losing the whole bot because one reaction landed on a
22
+ * deleted message, or one controller could not be resolved, is a worse failure than
23
+ * the one that caused it -- every other user is served by the same process.
24
+ *
25
+ * Nothing is silenced. The error is logged against the event that produced it, so a
26
+ * genuine misconfiguration -- an unbound controller, a missing dependency -- shows
27
+ * up on the very first interaction rather than staying hidden.
28
+ *
29
+ * @param event - The gateway event being handled, named in the log.
30
+ * @param run - The handler to run.
31
+ */
32
+ private runListener;
33
+ /**
34
+ * Rotates the bot's activity.
35
+ *
36
+ * Guarded separately from {@link runListener}: this runs on a timer rather than an
37
+ * event, and a throw from a timer callback is an uncaught exception no listener
38
+ * wrapper can reach.
39
+ */
40
+ private updateActivity;
41
+ private getInstance;
42
+ /**
43
+ * Registers the Discord event handlers and logs in.
44
+ *
45
+ * Rejects when the login fails -- an invalid token, or Discord unreachable -- so the caller can
46
+ * stop with a non-zero exit code. A bot that never came online is a failed start, and a
47
+ * supervisor such as Docker's `restart: on-failure` or systemd can only tell if the process says so.
48
+ */
49
+ start(): Promise<void>;
50
+ registerCommands(): Promise<void>;
51
+ /**
52
+ * Every pattern-matched route, ordered most specific first.
53
+ *
54
+ * Built once and cached, so dispatch stays a single ordered walk with an early exit
55
+ * rather than paying to rank anything per interaction. Ordering here is what lets
56
+ * `gi-profile/summary/{ownerId}/{uid}` keep the ids it owns when
57
+ * `gi-profile/{uuid}/{uid}` would also match them — without it, the winner would be
58
+ * whichever controller happened to be registered first.
59
+ */
60
+ private componentRoutes?;
61
+ private getComponentRoutes;
62
+ /**
63
+ * Warns rather than throws: an app whose patterns overlap boots and works today, and
64
+ * refusing to start would turn a latent mis-route into an outage on upgrade.
65
+ */
66
+ private reportAmbiguousRoutes;
67
+ /**
68
+ * Every `@Autocomplete` handler, ordered so an option-specific handler is found
69
+ * before a command-wide one.
70
+ *
71
+ * Cached alongside the component table: autocomplete fires on every keystroke, and
72
+ * rebuilding the list per keystroke would put reflection on the hottest path the
73
+ * framework has.
74
+ */
75
+ private autocompleteRoutes?;
76
+ private getAutocompleteRoutes;
77
+ /**
78
+ * Dispatches an interaction, and makes sure a failure anywhere in that still reaches
79
+ * the person who triggered it.
80
+ *
81
+ * {@link executeCommand} already reports what a handler throws, but everything
82
+ * *before* the handler can fail too — resolving a controller through the container
83
+ * is the common case — and a component that fails there would otherwise look dead
84
+ * with nothing said to the user and nothing in the log.
85
+ */
86
+ private handleInteraction;
87
+ private dispatchInteraction;
88
+ /**
89
+ * The names a command interaction can be handled under, most specific first.
90
+ *
91
+ * Empty for anything that is not a registered command, which is how a component
92
+ * whose customId matched no pattern falls through to the unmatched warning instead
93
+ * of being looked up under a name it does not have.
94
+ */
95
+ private resolveNameRoutes;
96
+ /**
97
+ * Answers an autocomplete interaction from the `@Autocomplete` handler that claims it.
98
+ *
99
+ * Discord closes the window after three seconds and shows a loading state until
100
+ * something arrives, so an unclaimed option is answered with an empty list rather
101
+ * than left to time out -- a visibly empty menu is a better failure than a stuck one,
102
+ * and the warning says which option is missing a handler.
103
+ */
104
+ private handleAutocomplete;
105
+ /** Closes an autocomplete window that nothing else answered. */
106
+ private respondEmpty;
107
+ /**
108
+ * Runs a resolved command, shared by both dispatch paths so a pattern-matched
109
+ * component and a named slash command behave identically once the route is chosen.
110
+ */
111
+ private executeCommand;
112
+ /**
113
+ * Tells the user something went wrong, if the interaction can still hear it.
114
+ *
115
+ * A handler that replies and *then* throws is the common shape of a failure, and
116
+ * replying twice throws in turn -- out of the catch block, where nothing is left to
117
+ * handle it. Whatever the interaction's state, reporting an error must not be able
118
+ * to become a second, worse one.
119
+ */
120
+ private replyWithError;
121
+ private handleMessage;
122
+ private handleReaction;
123
+ private gracefulShutdown;
124
+ }
125
+
126
+ declare class MeoCordFactory {
127
+ private static logger;
128
+ static create(target: ServiceIdentifier): MeoCordApp;
129
+ }
130
+
131
+ export { MeoCordFactory };
@@ -39,6 +39,13 @@ declare class MeoCordApp {
39
39
  */
40
40
  private updateActivity;
41
41
  private getInstance;
42
+ /**
43
+ * Registers the Discord event handlers and logs in.
44
+ *
45
+ * Rejects when the login fails -- an invalid token, or Discord unreachable -- so the caller can
46
+ * stop with a non-zero exit code. A bot that never came online is a failed start, and a
47
+ * supervisor such as Docker's `restart: on-failure` or systemd can only tell if the process says so.
48
+ */
42
49
  start(): Promise<void>;
43
50
  registerCommands(): Promise<void>;
44
51
  /**