Skip to content
GitHub

4.1.0-beta.8 changelog

Published

Entries marked Breaking change a working bot; the migration guide says what to do about them. Every release of the line is in the changelog.

Minor Changes

  • #273 ebd546e Thanks @l7aromeo! - A message command can tell its author, in a direct message, what the channel doesn't show. Without a filter, a message command's unexpected error is only logged and a cooldown refusal is skipped silently, since a reply in the channel can't be private. Two options turn on a direct message instead, both off by default:

    • @MeoCord({ messages: { dmOnError: true } }) tells the author the command failed, naming the command, the channel and the server. The error is still logged.
    • @MeoCord({ messages: { dmOnCooldown: true } }) tells the author how long to wait, once per wait: retries before it ends send nothing more. The notice is counted in the app's cooldown store, so it holds across shards with a shared store.

    Only patterned message handlers are answered, and only when no filter handled the error. A command sent in a direct message is answered there. A member whose direct messages are closed is not told, at debug level. Guard, validation, usage and UserError replies are unchanged. The texts are meocord.dm.error and meocord.dm.cooldown, translatable like MeoCord's other texts, in the server's language.

  • #269 a18e948 Thanks @l7aromeo! - meocord/testing: createMockUser(props?) and createMockChannel(Class, props?) take values for the mock's properties, as createMockInteraction does, so createMockUser({ bot: true }) makes a bot and createMockChannel(TextChannel, { name: 'general' }) a named channel. Neither took any before, so a bot user took Object.assign. The managers of a mock channel, messages, threads and a thread's members, and a mock guild's bans, have a real, empty cache, as a guild's members, roles and channels do, where reading cache.size gave a stub object.

Patch Changes

  • #274 4827e0f Thanks @l7aromeo! - meocord start --dev runs one bot at a time. A build that finished while the previous bot was still shutting down, which happens when the watcher reports one change twice or a file changes again during a slow onShutdown, started a second bot at once, alongside the one stopping. Once that one exited, a third started. The second was left running and connected to Discord, and a restart or Ctrl+C no longer reached it. Now builds that finish during a restart start a single bot from the latest build, once the previous one has exited, and a Ctrl+C during a restart waits for the stopping bot and starts nothing.

  • #266 6be7120 Thanks @l7aromeo! - meocord start --dev restarts the bot once per change, and the new bot always starts. The watcher took the tsconfig copy MeoCord writes for each build for a change of its own and rebuilt right after the first build, restarting the bot while it was still logging in. A stop that lands during login also left the old process running: discord.js's destroy() does not settle while the gateway waits for READY, so the bot came online after "Shutting down bot..." and the replacement never started.

    • A stop during login now ends the start at once, and ends with "Bot has shut down" as any other stop does: no onReady hook runs, start() rejects with an error isExplainedError() recognises, and the process exits 0. The client is closed once its login completes. This also fixes a Ctrl+C during login in production, which did nothing until a second one forced exit 1.
    • Edits to tsconfig.json now rebuild and restart the bot under start --dev, as edits to meocord.config.ts do. A changed meocord.config.ts is compiled again before the rebuild, and one that does not compile, such as a file saved mid-edit, is reported and leaves the running bot in place instead of ending the session.
    • If an application still has not exited after its shutdownTimeout and a short grace period, start --dev kills it with a warning and starts the new build.
  • #265 2d1ca03 Thanks @l7aromeo! - An answer MeoCord fails to build is reported as a fault, not passed off as Discord refusing it. When the text of a usage reply, a UserError reply or the answer to an interaction's error cannot be written, or a presenter throws while building an error view, the bot logs an error naming the call, and observers are told the call ended in 'error' with that fault. Before, it was logged only at debug level as "Could not reply", and the message went unanswered with no trace at the default log level. A send that fails is logged at debug level only when Discord refused it, a DiscordAPIError such as a missing permission; any other failure is logged as an error. In a testing module, such a fault rejects dispatch(). respond(interaction).error() still never throws, and logs a presenter that fails as an error.

  • #275 7d70abf Thanks @l7aromeo! - Two JSDoc corrections. @On and @Once said an event handler runs with the app's global guards, interceptors and filters; its class's and its own run too, as they always have. MeoCordTestingModule.fromApp's remarks now say plainly that compile() runs no factory and init() runs each one the module provides, so a factory the test replaces never runs, and one it keeps runs at init().

  • #270 9cb86a4 Thanks @l7aromeo! - A mistake MeoCord refuses as the bot loads is reported as one line, and the bot exits 1. This covers an invalid customId pattern, a builder that fails, a @Cooldown it cannot count, and what MeoCordFactory.create() refuses, such as a message pattern or two handlers for one command. Before, each came out as Node's uncaught-exception report: MeoCord's own source line, a stack through its bundle, and often none of it pointing at your file. Now the line names the handler, and the source file where the stack shows it. Under start --dev the watch session keeps running, so the next edit rebuilds. Any other error keeps the runtime's own report.

    • Every refusal begins with what it is on, Class.method:, Class: or App: for @MeoCord options, followed by the decorator and the problem, such as SampleButtonController.handleButtonWithId: Invalid pattern … or App: @MeoCord({ guards }): null is not a class. A test that matches a refusal's whole text may need its expected message updated.
    • @Cooldown, @Validate given something that isn't a Standard Schema, and SetMetadata with a key MeoCord reserves now throw where they are applied, rather than where they are called, so the message can name Class.method. Written as decorators, both happen in the same statement. A composite made with applyDecorators that includes one of them throws where it is applied; one that is defined but never applied no longer throws.
    • New projects' src/main.ts calls MeoCordFactory.create() inside bootstrap(), and sets exit code 1 for any startup error. An existing main.ts needs no change: create() reports what it refuses itself, and marks it so isExplainedError() returns true.
  • #268 5a69c71 Thanks @l7aromeo! - meocord/testing: mocks read the data Discord always sends as Discord sends it. A mock guild's preferredLocale was an empty stub object, so t.forGuild(createMockGuild()), and a message command's usage reply on a mock message, came out in the translator's default language whatever the test meant. It is now 'en-US', and a guild's name, a user's username, a message's pinned, a channel's type and the rest have Discord's values: false for a flag, null for what may be absent, and snowflake ids. tag, displayName, createdAt and url are computed from them as discord.js does. createMockGuild takes name and preferredLocale, so createMockGuild({ preferredLocale: Locale.Indonesian }) gives a server that speaks Indonesian.

    An interaction with a guildId has its user as its member, and its guildLocale is the preferredLocale of the guild it is given, as Discord sends it, where it was always 'en-US'. A mock message has its author as its member in a server, and null in a direct message, and its inGuild() answers as discord.js's does, where it returned undefined. A test that reads a property a mock has no value for now gets that value instead of a stub object; one that relied on the stub there sets the property itself. commandName, customId and a message's content stay the test's to give.

  • #272 a8f454d Thanks @l7aromeo! - The JSDoc says where every pipeline stage runs, so the API reference can show it on each entry. @Guard, @Interceptor, @Catch and @Pipe gain the @pipeline tag their @UseGuard, @UseInterceptor, @UseFilter and @UsePipe counterparts already had, and so do GuardInterface, InterceptorInterface, ExceptionFilter, PipeInterface and DispatchObserver. The stage names are those of the pipeline figure on meocord.dev: @Observer runs at observers-start and observers-settled, and @Defer's second step at defer-lock.

  • #263 925225d Thanks @l7aromeo! - The JSDoc of ContextMenuCommandBuilder.setType, as MeoCord types it, says to leave a context menu builder's build() return type inferred. Written out as ContextMenuCommandBuilder, it drops the kind setType() gave, so a handler of the other kind compiles and is caught only as the bot starts.

  • #271 8b1d9db Thanks @l7aromeo! - A new app's package.json has a start script, meocord start --prod, so npm start, bun run start and a host that runs npm start for you, as many Node hosts do by default, start the production build. start:prod is unchanged, and building stays its own step: run build:prod before start. An existing app can add the line to its scripts to get the same.