4.1.0-beta.10 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
- Breaking
#295
a304494Thanks @l7aromeo! - Stop a bot from code withapp.stop(). It runs theonShutdownhooks under yourshutdownTimeoutand closes the client, so an owner-only shutdown command, a graceful restart or an integration test needs no signal. A bot in one process keeps its process running. With process sharding, the manager'sstop()asks every shard to shut down and waits for it, and keeps the manager running. Called in a shard,stop()asks the manager to stop every shard, so it stops the bot whichever process calls it; that shard's process ends with the others. A stop while the bot logs in ends that login, so itsstart()rejects. Calls after the first wait for it, and a stopped app does not start again: useMeoCordFactory.createto make a new one. A client that fails to close, or a shard the manager has to kill, setsprocess.exitCodeto 1, unless another code is set, so the process exits as a signal's shutdown would. A signal, or a shard manager's request, that comes whilestop()runs waits for its hooks to finish rather than exiting at once.TypeScript const app = MeoCordFactory.create(App) await app.start() // later await app.stop()A SIGINT or SIGTERM after a failed login now exits with the code the failed login set, 1, where 4.0 exited 0, so a process supervisor no longer reads a bot that never came online as a clean stop. See the upgrade guide.
- Breaking
#310
79015cfThanks @l7aromeo! -applyDecorators(A, B)now applies its decorators as@A @Bdoes, stacked in the order written:Bfirst, thenA. It applied them the other way round, so guards listed in it ran in the reverse of the order written, and moving stacked decorators intoapplyDecoratorschanged what ran first. A method or class a decorator returns in place of the one it was given, as a wrapping decorator does, now reaches the next decorator and TypeScript; it was dropped.applyDecorators(UseGuard(A), UseGuard(B))now runsAbeforeB. To keep 4.0's order, writeapplyDecorators(UseGuard(B), UseGuard(A)). The same holds for interceptors, pipes and filters composed this way. See the upgrade guide. #326
a3c2a2eThanks @l7aromeo! - A catalog message shows a brace as text when it is written twice, and a translator says when a key has no message.{{and}}are one brace each, when the code compiles and when translating, so'Buttons use ticket/{{id}}'showsticket/{id}and takes noidparam. A 4.1 beta catalog that writes{{name}}for a param inside braces now shows{name}as written; write{{{name}}}instead.localizations(key)takes only a message without{params}, as Discord shows a name or description as written. Another key fails to compile, or, in a catalog the compiler can't read, such as a JSON file, throws as the app loads. A translation with a{param}is left out, so Discord shows the default for that locale, andexpectCompleteCatalogreports it.- In development, a key with no message, or one naming a group of messages, logs a warning once, since the key is shown in its place.
See Localisation.
#399
24f33e2Thanks @l7aromeo! - Fixes inmeocord/common:- A send Discord could not read is logged as an error. When a reply, an edit, a direct message or an acknowledgement fails, MeoCord still logs a refusal for something Discord reports at debug level, such as a missing permission, an interaction already answered or a message already gone. A body Discord could not read is logged as an error with its cause: an invalid form body (
50035), invalid JSON (50109) or an empty message (50006). Only the code that built the request can fix those, and they were logged at debug, where they went unseen. RedisCooldownStoreon Redis Cluster counts a call against all of a handler's keys or none. When a handler's cooldowns sit in different slots, each key is counted by a script of its own. If a later key's script fails, the uses already counted are now given back before the failure is reported, so the call does not leave earlier keys counted for a call that was refused.RedisCooldownStore's error for a reply it cannot read describes the reply it expects, including the wait's end on a refusal.meocord/commonexportsResponseLockOptions, the optionsrespond(interaction).lock()takes, so a helper that passes them on can type them.MemoryCooldownStorekeeps its key count and its sweep to itself. It dropped expired keys once a minute through a publicsweep(), beside asizegetter, both of them for MeoCord's tests. Neither is part of the store's API, and code that called them uses the store as any otherCooldownStore.- A catalog's
meocordgroup with a text where MeoCord has a group is refused with one message naming it, such as "meocord.usage is a group, not a text", where it named every property of a string.
- A send Discord could not read is logged as an error. When a reply, an edit, a direct message or an acknowledgement fails, MeoCord still logs a refusal for something Discord reports at debug level, such as a missing permission, an interaction already answered or a message already gone. A body Discord could not read is logged as an error with its cause: an invalid form body (
#349
380a2a8Thanks @l7aromeo! - A call refused because the cooldown store answered too late no longer costs its caller a use. UndercooldownStoreFailure: 'deny', the default, a store slower thancooldownStoreTimeoutMsrefused the call with "try again shortly", yet its late answer still recorded it, so the retry was told to wait the whole window although the handler never ran: a/dailywas lost. Now@Cooldowngives that use back.CooldownStore.consumeManymay return a verdict withrelease(), which undoes the call it recorded.MemoryCooldownStore,RedisCooldownStoreandShardedCooldownStoregive it, and@Cooldowncalls it for any call it has already refused when the late answer arrives. A store of your own can add it to the verdict itsconsumeManyreturns; one without it keeps such a call counted, as before, andtestCooldownStorechecks either. Under'allow', the call ran uncounted, so the late count is its own and stays.RedisCooldownStorenow stores each call under its nonce alone, whichreleaseremoves; calls recorded before the upgrade leave their windows as usual. On Redis Cluster, where a handler's keys sit in different slots withouthashTag: 'handler', each key is counted by a script of its own: a call counted that way is given back on every key, and a cooldown that refuses it gives back the keys counted before it, so the call counts against all of them or none, as on a single node.#372
efe4518Thanks @l7aromeo! - A cooldown store's refusal can say when the next call is allowed:CooldownVerdict.retryTimestamp, a Unix timestamp in milliseconds on the store's own clock. Every refusal in one wait gives the same one.MemoryCooldownStore,RedisCooldownStoreandShardedCooldownStoregive it, andmessages.dmOnCooldowntells one wait from the next by it. A store of your own can add it to its refusals; without it, waits are told apart byretryAfterMsand the bot's clock, as before.testCooldownStorechecks a store that gives it.#352
5667989Thanks @l7aromeo! - A cooldown's wait is shown as a Discord timestamp, so a daily cooldown no longer says "try again in 1440m". The built-in answer to a blocked call, in replies, DMs and the presenter, is the newmeocord.cooldown.untiltext, "Slow down: try again {when}.", where{when}is<t:…:R>: Discord words it in the reader's language and counts it down. The time is rounded up to the second, so it never reads as now while the call is still refused.For a bot upgrading from an earlier 4.1 beta:
meocord.cooldown.seconds,meocord.cooldown.minutesandmeocord.cooldown.wholeMinutesare gone. Translatemeocord.cooldown.untilinstead, keeping{when}.CooldownErrorgainsretryAt, theDatethe next call is allowed, andlimit, theusesandwindowMsof the cooldown that blocked the call, so a filter or presenter needs no arithmetic:time(error.retryAt, 'R')from discord.js gives the same timestamp. Itsmessage, which reaches logs and tests, stays plain text, now in the two biggest units that fit, andcooldownMessage()gives the same. For a wait of an hour or more that changes what it says: "Slow down: try again in 1439m." now reads "Slow down: try again in 23h 59m.", so a test that matches it changes too.#376
5a58dc1Thanks @l7aromeo! -@MeoCord's options and@Validate's pipes have names you can import:MeoCordOptions, frommeocord/decorator, is what@MeoCordtakes, each option documented where your editor shows it as you write the object. Name a base two app classes share with it; its guard, interceptor and filter lists are still checked against the classes they hold.ValidatePipes<S>, frommeocord/interface, is the pipes@Validatetakes for schemaS, so a decorator of your own that wraps@Validatechecks the pipes it passes on against the schema, as@Validatedoes.
A misused decorator's message names it more exactly:
@Commandcalled with the wrong interaction names the builder it was declared with, as@Command('stats', StatsBuilder), and puts the right article before the class,an AutocompleteInteraction; a stage entry that is a string is quoted,"Allow" is not a class, and one that is a list, asguards: [[StaffGuard]]writes, is named as one,an array is not a class, rather than{ provide } does not name a class.- Breaking
#296
90d859cThanks @l7aromeo! -ReactionEventnames the second argument a@ReactionHandlermethod receives,{ user, action }.ReactionHandlerOptionsstays as a deprecated alias of it: every other…Optionstype is something you pass in, to a decorator, a function or a constructor.These are deprecated, and removed in the next major version (5.0). Each still works in 4.x, and its JSDoc names what to use instead:
ReactionHandlerOptions: useReactionEvent. See the upgrade guide.SetMetadata: usecreateMetadata. It logs a warning once.ExecutionContext.get(key)andgetAll(key)with a string or symbol key, and the same on aHandlerRegistryentry: pass a decorator made bycreateMetadata. They log a warning once. See the upgrade guide.respond()'sephemeraloption: useflags: MessageFlags.Ephemeral. It logs a warning once.Themeand its colours: readuseTheme().colors, and set the colours in@MeoCord({ theme }). Reading aThemecolour now logs a warning once too, as assigning one already did. See the upgrade guide.MetadataKey,CommandMetadataandAutocompleteMetadata: internal names with nothing to use instead. In the next major version (5.0) they are no longer exported. See the upgrade guide.
Run ESLint with
@typescript-eslint/no-deprecated, as a new app's config does, to find every use in your code. #367
a497b6eThanks @l7aromeo! -meocord start --devfixes:- A save that doesn't compile leaves the bot running. A build with errors emitted a bundle that throws them, and the bot restarted onto it. Now a build with errors emits nothing and restarts nothing, and the bot keeps running its last good build until the code compiles again.
- Ctrl+C while watch mode restarts the bot joins that shutdown, so the bot's
onShutdownhooks finish and the session exits 0. A second Ctrl+C still stops it at once.
A new app reads the same .env files on every runtime and however it is started,
meocord startornode dist/main.jsunder pm2, systemd or Docker:.env.<mode>.local,.env.local(not undertest),.env.<mode>and.env, a more specific file winning and the shell over all of them, as Bun reads them. An app made before this keepsimport 'dotenv/config', which reads.envalone under node, as 4.0 did. To read them all, replace that import inmeocord.config.tswith:TypeScript import { config } from 'dotenv' const mode = process.env.NODE_ENV || 'development' config({ path: [`.env.${mode}.local`, ...(mode === 'test' ? [] : ['.env.local']), `.env.${mode}`, '.env'], quiet: true, })On Bun, set
NODE_ENV=productionwhere you start a production bot yourself, as withbun dist/main.jsunder pm2, systemd or Docker. WithNODE_ENVunset, Bun loads.env.developmentand.env.development.localbefore any code runs, and dotenv keeps what is already set, so their values win over.env.production. The bot now warns when that happens, naming the variables that hold a development value: "Bun loaded .env.development because NODE_ENV is unset, and this is a production build, so API_URL has its development value; set NODE_ENV=production, or start withbun --no-env-file."meocord start --prodsetsNODE_ENV=productionalready.meocord build --prodcompilesmeocord.config.tsin production mode, as it builds the bot, soprocess.env.NODE_ENVin the config readsproductionin a production build however the bot is started. It readdevelopment, the mode the config was always compiled in. So a config that branches onNODE_ENV, such as to register commands to a development guild, now takes its production branch in a production build: check what that branch does before you deploy, and rebuild to pick this up.#301
a2991bfThanks @l7aromeo! -meocord/eslintturns on@typescript-eslint/no-floating-promises. A promise nothing awaits, such asrespond(interaction).send()or a database write left withoutawait, rejects outside every handler MeoCord runs, so its error reaches no exception filter and can end the bot. An interceptor'snext.handle()left that way runs the code after it before the handler starts, so that code never sees the handler's result or error.After upgrading,
bun run lintmay report calls like these in your code. Each one is a promise that runs on its own:awaitit, orreturnit, where the code after it should wait, as an interceptor'snext.handle()always should;- or write
voidbefore it where it is meant to run on its own, and handle its failure with.catch().
To keep the previous behaviour, set the rule to
'off'in youreslint.config.ts.#320
404ba3cThanks @l7aromeo! -getResponse(interaction)reports every answer a mock interaction got, whether the handler made it throughrespond()or with discord.js directly, such asinteraction.reply()orinteraction.followUp(). Before, it reported only whatrespond()made, so a handler written with discord.js's own methods showedsent: truewith nocalls, and a test had to read the mock's methods instead. Each call appears once, in the order made, with what it sent, withoutwithResponse, and theerrorof one Discord refused. A test asserting the oldcallsof a handler that mixes the two now sees the direct calls too.#373
e01776aThanks @l7aromeo! -meocord/commonexportsLocalizationKey<C>, the keysTranslator.localizations()takes: a single string with no{params}. The method's parameter used that type without a name you could import, so a helper that forwards a key tolocalizations()could not type it.StringMessageKey<C>'s documentation no longer says it is the type command names and descriptions need, andTranslatorOptionsandCatalogDefinitionare listed with the other localisation types.#312
32ede56Thanks @l7aromeo! -meocord/testing's mock channels, and the managers that fetch, answer as discord.js does, andcreateMockMessagetakes thechannelit was sent in.- An interaction's
channelis a text channel of its server, the one the server caches underchannelId, or the user's DM channel in a DM. It was a stub, whosesend()andisTextBased()threw. Achannelgiven still wins. - A message's
channelis a text channel of its server, cached there, or the author's DM channel for a DM. It was a bare text channel with no managers. PasscreateMockMessage({ channel })to send it in another. - A mock channel's type guards, such as
isTextBased(),isDMBased(),isThread()andisSendable(), run discord.js's own logic. They returnedundefined, soif (!channel.isTextBased()) returnreturned early. A voice channel carries its text chat, and a forum its tags, as discord.js tells them apart by. - A manager's
fetch(id)resolves to its cached item with that id, such as a member given tocreateMockGuild({ members }), or to a new one with that id, which it caches. It returned an item with another id. A member fetched from a guild'smembersis in that guild.
A test that relied on one of the old answers changes with it; nothing changes in a bot.
- An interaction's
#291
836e026Thanks @l7aromeo! -meocord/testingaddscreateMockMember({ user, guild, roles, nickname }), and its mock users and members answer as discord.js does.createMockMember()makes a member with the roles given.roles.cacheholds the server's @everyone role, then those roles;roles.add(),remove()andset()change them and resolve to the member;roles.highestis the role that ranks highest, by position, then the lower id.permissionsare its roles' permissions combined, @everyone's included, or every permission for the server's owner. Pass the member tocreateMockGuild({ members }), and an interaction or a message from its user in that server has it as itsmember. See Mocks.- Every mock member has a
rolesmanager andpermissions, with only @everyone unless given roles (for an interaction with aguildIdbut noguild, an @everyone with that id), instead of stubs that threw onroles.cache.has()orpermissions.has(). A mock role has an id,position1, above @everyone, and no permissions unless given. - A mock guild has an @everyone role,
roles.everyone, inroles.cache: the role given tocreateMockGuild({ roles })with the guild's id, or one at position 0 with no permissions. A guild'sroles.cachetherefore holds one more role than the roles given, unless one of them has the guild's id. - An interaction in a server has the member's permissions as
memberPermissions, andnullin a DM, instead of a stub whosehas()threw. - Every mock user is a person,
bot: false, however it is made.createMockInteraction(User)gave a truthybot, so a message from that user reached no handler. - A DM sent to a member goes through its user's
send(), and a user's through its one DM channel, souser.send,member.sendand(await user.createDM()).sendeach see it.createDM()resolves to that channel; it returnedundefined. - In
createChatInputOptions, a user option'sgetMember()is the user's member in the interaction's server, andnullin a DM, instead of the user; a member given resolvesgetUser()to its user. A whole number is an Integer option and a fraction a Number one:getInteger()returned1.5for a fraction, which only a Number option holds, and now returnsnull.
A test that relied on one of the old answers changes with it; nothing changes in a bot.
#324
3b41afdThanks @l7aromeo! -meocord/interfacenames the options of the stage decorators,GuardOptions,InterceptorOptions,ObserverOptionsandValidateOptions, so a decorator of your own that wraps one can type what it passes on, instead ofParameters<typeof Guard>[0]. It also exportsPrimaryEntryPointCommandData, the body an entry point command's builder returns, which could otherwise be written only asCommandBuildResult<CommandType.PRIMARY_ENTRY_POINT>.#354
1eb4fa0Thanks @l7aromeo! - A presenter can draw its views, such as with a canvas library, and attach what it draws:ResponseView.filescarries files, anAttachmentBuilderor{ name, data }, which MeoCord sends with the view and shows for you. In an embed, the first image is the embed's image. In a Components V2 container, images go in galleries of up to 10 below the text, and other files as file components. A file the view's owncomponentsshow byattachment://<name>is not shown again.ResponseView.imageandthumbnailname one of the files, or a URL, to use as the embed's image or thumbnail, or the container's leading image and the text's thumbnail.loading()anderror()may return a promise. A slow drawing never misses Discord's three seconds. The loading view is drawn after@Deferacknowledges the click. An interaction not yet acknowledged is acknowledged privately before a drawn error view, which then replaces the acknowledgement; should that drawing fail, MeoCord's own view answers instead. Discord refusing the acknowledgement, as for an interaction past its three seconds, is logged once as the refused send, never as the presenter failing, and an interaction Discord no longer knows is not answered again.- A view past Discord's limits is sent without its files, with a warning: more than 10 attachments on the message, counting those it keeps, or a file over the interaction's attachment size limit, 20 MiB without one. A send Discord refuses as too large, as one with a file whose size could not be checked first, is sent again without its files. The user still gets the answer.
- A presenter's optional
messageError()draws a message command's error replies: the usage reply, a guard's or validation's reason, aUserError's message, and the DMsdmOnErroranddmOnCooldownsend, as embeds with its files. It receives aMessageResponseContext, with themessagein place of an interaction. A presenter without it, such as the one a new app is generated with, leaves those replies plain text, exactly as before. ShouldmessageError()throw, reject or return a view an embed cannot hold, the reply or DM is sent as that plain text, and the failure is logged as the call's fault, so a testing module'sdispatchrejects with it.
Edits that add a view's files keep the message's own attachments, and a drawn loading view's files leave when the lock does.
#377
f543e78Thanks @l7aromeo! -ShardContext.calltypes each result as it arrives: through JSON, as every mode passes it.meocord/coreexportsJsonified<T>, the type a value has after that trip: aDateis astring, aMapor aSetis{}, a class withtoJSON()is what it returns, and a function orundefinedproperty is left out. A method returningPromise<Date>now givesShardCallResult<string>[], where it saidDateand gave a string. Code that read the value as the method's own type changes to the JSON form.The arguments take the same trip, so
call()refuses a method whose params JSON would change, such as one taking aDate: the compile error says the argument arrives as JSON and names the type to declare (stringfor aDate). Anundefinedargument now arrives asundefined, where it arrived asnull.ShardContextis new in 4.1, so only beta users see these changes.#350
1f39632Thanks @l7aromeo! -MeoCordTestingModule.create()andfromApp()takeshutdownTimeout, which mirrors the option of that name inmeocord.config.ts: from 0 to 2147478647 ms, refused otherwise, with the messagemeocord.config.tsgets for it.close()runs the bot's own shutdown sequence, so before the cooldown store shuts down, the callsinvoke,dispatchandemithave under way finish. It waits up toshutdownTimeout, 10 seconds unless set, for the calls, the store's operations and theonShutdownhooks. Then it stops waiting and logs that it did, as the bot does, and still rejects with any hook that failed before then. A test whose fake store never answers, or whoseonShutdownnever settles, sets it short:TypeScript const module = MeoCordTestingModule.create({ app: App, providers: [{ provide: CooldownStore, useValue: silent }], shutdownTimeout: 50, }).compile()#325
c5ca2b9Thanks @l7aromeo! -createTranslatoranddefineCatalogread ascreateTranslator(options: TranslatorOptions<Locales, Default>)anddefineCatalog(catalog: CatalogDefinition<T>)in your editor and on the API pages, instead of spelling out the compiler checks they make.TranslatorOptionsandCatalogDefinitionare exported frommeocord/common, each documented with what it refuses. The checks are unchanged. Nothing to change in your code.
Patch Changes
- Breaking
#297
94b573cThanks @l7aromeo! - A bot withoutactivitieskeeps the presence it sets. 4.0 rotated throughactivitiesevery 10 seconds whether or not any were set, and with none it cleared the bot's activity each time, so a status set inonReady, inclientOptions.presenceor by a command disappeared 10 seconds after ready, and an empty presence update was sent every 10 seconds. Now MeoCord touches the presence only whenactivitieslists some, and shows one as soon as the bot is ready rather than 10 seconds later. Withactivitiesset, they cycle in order, as documented: the first once the bot is ready, then the next every 10 seconds, starting again after the last. 4.0 picked one at random each time. A timer that set the status again to work around it can go. See the upgrade guide. #329
94c8b2cThanks @l7aromeo! - An asset import resolves beside the bundle, whereverdistis run from.import logo from './logo.png'gave the absolute path of the folder the build ran in. So adistbuilt in CI, on a laptop and then copied to a server, or in an image stage with anotherWORKDIR, read its assets from a path that wasn't there, and failed with ENOENT at the first attachment. The path is now set when the bot starts, from the bundle's own location. It's still an absolute path on disk, now the right one. Rebuild to pick this up.#314
4c0a815Thanks @l7aromeo! -bundleDependenciespacks every package a bot needs intodist/node_modules, each with the dependency versions it was installed with, from a pnpm project as from npm, yarn and bun.- A package listed in
externalsoroptionalExternalswas packed without the packages it depends on, in a pnpm project. pnpm keeps those beside the package in its store, not in your project'snode_modules, so they were left out, and the bot failed at startup with "Cannot find module". They're packed now. - A native package that ships its binary as a per-platform package, as napi-rs packages do, is recognised as native in a pnpm project and packed with that binary, whether you list it in
externalsor the bot imports it. Before, a listed one was packed without its binary, and an imported one stopped the build, asking for it inexternals. - Each packed package loads the version of each dependency it was installed with. When packages need different versions of one package, the first stays at the top of
dist/node_modules, and each other version is nested where Node, resolving from the package that needs it, finds it first. Before, every package got the first. - A package npm nested under another, because another version of it holds the top of
node_modules, is packed with the packages it needs from the top. Before, they were left out, and the bot failed at startup with "Cannot find module".
Rebuild to pick these up.
- A package listed in
#316
d2284d9Thanks @l7aromeo! -respond()could see a locked message's content as changed when it wasn't. It compares the message with what MeoCord last wrote by sorting keys with the runtime's collation, which ranks two different keys equal when they differ only in Unicode normalization, such ascaféwritten withéand witheand a combining accent. Their order then followed the input, so equal content could compare unequal. Keys are now sorted in code-unit order, which needs no locale. This also no longer loads the runtime's ICU data on the first lock or restore. Nothing to change in your code.#395
d5d07cbThanks @l7aromeo! - CLI and build fixes:- Every imported file lands in
dist/assetsunder its own name. A pdf, txt, webmanifest or wasm import was written todist/static/assetswith a content hash, unlike images, fonts and media. Its import gives that path as before, so nothing in your code changes; rebuild to pick it up. Two imported files of one name in different folders stop the build with Rspack's conflict error, naming the file. meocord start --devruns the bot on the development .env files whateverNODE_ENVyour shell holds, as its development build's config reads them: it watches them, and starts the bot withNODE_ENV=development. WithNODE_ENV=productionin the shell, it watched the production files, and a bot on Bun read the production values.- A production bot on Bun warns about another mode's .env values for any
NODE_ENVbutproduction. Bun reads the development files for everyNODE_ENVexceptproductionandtest, so a bot started withNODE_ENV=stagingran on development values without the warning an unsetNODE_ENVgets. meocord createwarns on Node 22.0 to 22.12, below the>=22.13the package requires. It compared the major version alone.- The CLI finds an installed package in the filesystem root's
node_modules, as with a project directly under/in a container.
- Every imported file lands in
#319
218b6c5Thanks @l7aromeo! - The CLI clears the screen only where it helps: asmeocord start --devbegins, in a terminal.meocord buildandmeocord start --prodno longer clear it, so the output of the commands before them, such as a failing test run, stays on screen. No command writes escape codes into piped output any more, such as CI logs,docker logs, pm2 or systemd. Whenstart --devclears, it keeps your scrollback.A build's output no longer lists the temporary folder the config is compiled in. The line after it still says where the config went.
#391
f8dcc1aThanks @l7aromeo! - Corrections to comments that ship with the package and in a new app; no behaviour changes.- The
meocord/eslintexample ignores generated code, in place ofcoverage, which the config already ignores. - In a new app, the comments in
vitest.config.tsgive the reasons that apply to the versions it installs,src/types/theme.d.tsexplains the theme augmentation in three lines, and the sample message controller's TODO reads correctly. The sample slash, modal and guard spec titles say only what their tests check. meocord generate: the guard template lists'autocomplete'among the handler kinds it can limit itself to, the interceptor template says interceptors skip autocomplete, and the filter template saysresponseis undefined for autocomplete too.
- The
#364
23514e7Thanks @l7aromeo! - The CLI reads the config the bot it runs reads.meocord start --devuses theshutdownTimeoutandsourceMappedStacksof the config it last compiled, so an edit tomeocord.config.tsapplies from the next restart. Before, it kept the ones from whateverdistheld when the session began, and waited a shortershutdownTimeoutthan the bot's own, killing it before itsonShutdownhooks finished. The token check follows the same rule.startandregistercheck the source config when they build first. Otherwise they check the compiled config the bundle runs, someocord registerwithout--buildnow stops at a compiled config that fails to load, asstart --proddoes.#394
bfb3e07Thanks @l7aromeo! - The editor documentation ofmeocord/commonandmeocord/interfacematches what the code does:respond()'s state.messageis the last reply, update or edit, never a follow-up.lock()withdisable: 'none'leaves the message alone.error()puts a locked message back as it was before it follows up.- Errors.
GuardDeniedError,ValidationError,CooldownErrorandCooldownStoreErrorsay how a message command is answered. A message command gets a reply in the channel for a denial or invalid input. For a cooldown or a store outage it gets nothing, or a direct message undermessages.dmOnCooldownormessages.dmOnError.CooldownStoreErroralso says that the recovery is logged once the store has answered for 30 seconds without failing. The constructors and helpers have their@paramand@returns. ExecutionContext.getController()is the class the handler runs on, the subclass for an inherited one.getParams()covers pipes too.- Cooldown stores.
MemoryCooldownStore,RedisCooldownStore.usingand the Redis constructor document all their parameters,hashTagincluded. On Redis Cluster, a refused call counts against no key unless giving a use back fails. route().build()also throws aTypeErrorfor a value that is not of its param's type.- Localisation.
createTranslatordocumentsoptionsand what it returns.defineCatalogsays which catalogs need it.LocaleCatalogsays where a locale's{params}are checked.- A plural needs its
otherform to be read as one.
createTokennames what its type checks:TestingModule.getand a provider'suseValueoruseFactory.Logger. A string prints in its tag's colour. Each method documentsargs.- App options.
caseSensitivecovers choice words and flag names.deleteUsageRepliesAftercovers a guard's or validation's reason.replyEmojinotes that help begins with the info emoji.helpandMessageHelpsay that!helpleaves out hidden and guarded commands.scope: 'dm'with a server-only param is refused.
OnShutdownruns onapp.stop()too.- Examples. The examples for a param type's and a theme's
declare moduleimport what they use.
#302
e5724e8Thanks @l7aromeo! - A component handler's params are checked against what a call gets, when the code compiles:- A select menu's choices have their real types:
values: string[],users: User[],members,rolesandchannelsas discord.js resolves them. A declaration no choice can have, such asvalues: numberorusers: string, fails to compile, where it used to compile and then fail on the first selection. A narrower type that a choice can hold, such asmembers: GuildMember[]orreadonly Role[], still compiles. - A plain-string customId pattern's keys are checked as a
route()'s are. With@Command('stats/{id}', CommandType.BUTTON), a handler declaring{ uid }fails to compile, naminguid, since that param was alwaysundefined. Fix the name to one the pattern captures. - A key a pipe produces is declared
Piped<T>, as with@Validate, and the check leaves it to the pipe. That covers a choice@UsePipeturns into something else, a typed customId param such as{id:int}piped into an object, and a@MessageHandlerpattern's param piped the same way, which no declaration could satisfy before. The check reads only the handler's own type, so it cannot see the pipe. A 4.1 beta handler that writes@UsePipe('values', ToQuantities)with{ values: number[] }now fails to compile, with a message ending "a key a pipe produces is marked Piped"; write{ values: Piped<number[]> }instead.
- A select menu's choices have their real types:
#317
eb6698eThanks @l7aromeo! - A built bot finds its config beside its bundle, wherever it's started from. Before, it looked indistunder the working directory, so a bot started from elsewhere failed with "MeoCord config not found … Runmeocord build", even with a fresh build. That covered pm2 withoutcwd, a systemd unit withoutWorkingDirectory, andcd dist && node main.js.When the config really is missing, the message names the file it looked for and the working directory. When the file is there but fails to load, the message says so, gives the reason and says what to do: install the package it names when one is missing, naming the installed package that imports it if the config does not import it itself, or fix
meocord.config.ts, then runmeocord build..envis still read from the working directory, throughdotenvin yourmeocord.config.ts. If you start the bot from elsewhere, set its environment there or pointdotenvat the file.The check that refuses a build made for another platform also reads its record beside the bundle. It now applies when a process manager's wrapper starts the bot.
A development build (
meocord build --dev) does the same, also when a process manager such as pm2 starts it through its own wrapper: its config, its asset imports and the script its shards start from are all found beside its bundle.Rebuild to pick these up.
#363
b38234cThanks @l7aromeo! - A compiled config that fails to load is reported once, with its reason and what to do: "MeoCord config at … failed to load: . Fix meocord.config.ts, then runmeocord build.", or, when the config needs a package that isn't installed, one that says to install it. A built bot printed a separate "[MeoCord] Failed to load …" line before that one, andmeocord start --prodprinted it a third time.meocord start --prodwithout--buildnow stops with that message, rather than checkmeocord.config.tsin its place, which could stop on a missing token without naming the broken config the bot would run.- Breaking
#313
0060a3cThanks @l7aromeo! - Three mistakes with@Commandand@MessageHandlerare now named where they happen:- A command builder whose constructor throws, such as one reading a translator or the environment in a field, is refused as the class loads, like a builder whose
build()throws:Stats.stats: StatsBuilder could not be made for "stats": missing translator.It used to surface as the bare error at import, naming neither the builder nor the command. - A
@Commandhandler called with another kind of interaction, as a direct call in a test can be, throwsCards.card: @Command('card/{id}', CommandType.BUTTON) takes a ButtonInteraction, not a ChatInputCommandInteraction., or…; it was given undefined.for something that is no interaction at all, instead ofInvalid interaction type passed to @Command for method: card. @MessageHandler('')still runs for every message, as@MessageHandler()does, and now logs a warning naming the handler:@MessageHandler('') on Chat.every is deprecated; in the next major version (5.0) it is refused. Use @MessageHandler() instead.See the upgrade guide.
A builder error that ends in a full stop no longer gets a second one in its refusal.
- A command builder whose constructor throws, such as one reading a translator or the environment in a field, is refused as the class loads, like a builder whose
#344
7b68f81Thanks @l7aromeo! -@Cooldown'ssecondsis now counted in whole milliseconds, and a window no store can count is refused where the decorator applies.- A
secondsvalue whose milliseconds weren't a whole number, such as16.1, madeRedisCooldownStorefail every call to that handler with "ERR value is not an integer or out of range", while the memory store used in development and tests counted it fine.secondsis now rounded to the millisecond, once, so every store gets a wholewindowMsof at least 1. secondsmust be from0.001to4320000000000.Infinityand other values beyond that were accepted, and each store did something different with them: the memory store refused with "try again in Infinitym NaNs", Redis failed every call, and process sharding never limited anything.@MeoCord({ cooldownStoreTimeoutMs })must be at most2147483647, the longest delay a timer keeps. A longer one fired at once, so every call with a cooldown timed out.testCooldownStorechecks a store that keeps the defaultconsumeManyorpeekManyagainst what those defaults do, where its batch and peek cases passed without checking anything. A store whoseconsumelets two concurrent calls take the last use now fails the concurrent batch case as well.- The
cooldownStoreTimeoutMsrefusal showsInfinityandNaNas they are, rather than asnull.
- A
#348
63f9514Thanks @l7aromeo! - A cooldown's count is kept under a key named for its window, not its position among the handler's cooldowns. A release that added, removed or reordered a@Cooldownmoved the counts a persistent store such as Redis kept to other cooldowns: adding a short cooldown above a daily one reset the daily for everyone, and a short one could inherit a long one's history. Now a deploy that adds, removes or reorders cooldowns leaves the others' counts where they are. Changing a cooldown'suseskeeps the calls counted so far, held to the new number; changing itssecondsstarts its count again. A handler's cooldowns with the sameseconds,per,byandbypasscount the same calls, so they share one count, held to the smallestuses. The exception is two cooldowns over the samesecondsandper, both withbyor both without, whosebyorbypassfunctions differ (two inline functions differ even when written alike):usestells them apart, so for those, changinguses, or adding or removing another such cooldown, starts their counts again, and reordering two with the sameusesswaps their counts. A call thatbyreturnsundefinedfor is counted apart from a cooldown with nobyover the same window. For a bot upgrading from an earlier 4.1 beta, counts kept under the old keys are not carried over, so every cooldown's count starts afresh once.#345
a3358e4Thanks @l7aromeo! -messages.dmOnCooldownnow DMs an author once per wait, as documented, however often they retry within it. The notice was counted over the wait left at each refusal, which shrinks with every retry, so it expired halfway through: an author retrying every second during a 60-second wait got six DMs (at 1, 31, 46, 53, 57 and 59 seconds), and about twelve in an hour-long one. Each wait now has a notice of its own, kept for the cooldown's full window. A wait is told apart by when it ends on the store's own clock, so a retry whose answer comes back late, or from another shard sharing the store, finds the notice already taken.#346
3f9d6f7Thanks @l7aromeo! - A cooldown store that fails some calls and answers others, as a Redis Cluster with one node down does, is now logged as one outage. Any answer ended the outage and the next failure began a new one, so each failing call logged an error with its stack and then a recovery line. An outage now ends when the store answers 30 seconds or more after its last failure, and the recovery line counts every call that failed in it.#350
b36cba3Thanks @l7aromeo! - A class you bind with@MeoCord({ cooldownStore })now gets itsonReadyandonShutdownhooks, as a service does. Before, neither ran, so a store that opens a connection inonReadyand closes it inonShutdownnever connected and leaked its connection on every shutdown.The order suits a store that connects:
- Its
onReadyruns before the services'. A call that comes while it runs waits for it, withincooldownStoreTimeoutMs. One that would wait longer meets yourcooldownStoreFailurepolicy, as a store that doesn't answer does. - Its
onShutdownruns after the services'. The bot first stops taking new calls and lets the ones under way finish, along with every store operation they started, even an answer that came after its call stopped waiting. So the store closes after the last write it is asked for. - What the store injects, such as the queries it runs, is ready before it and shuts down after it, so it is there for the store's last write.
- A store with no
onShutdown, and nothing it injects with one, doesn't hold shutdown up: the bot waits for the calls under way only when a hook needs the store.
MeoCordTestingModuleruns the store's hooks in the same order, ininit({ ready: true })andclose(), for the app's store or theCooldownStorea test provides in its place.- Its
#393
1403136Thanks @l7aromeo! - Corrected the editor documentation of several public APIs to match what they do:HandlerRegistry:list()gives the handlers in the order the app makes its classes, not the order they were bound.messageHelp()lists by where the message was sent, not by its author, and an entry'shiddenleaves a command out of help's lists while!help <command>still shows it.useTheme(): outside a call it reads the app's theme from when its start begins until it has shut down. The chain of@UseThemestops whereinheritStages: falsedoes, and only the theme's plain objects and arrays are frozen.MeoCordFactory.create: with process sharding, the manager logs an error a shard throws.ShardContext:runHeretakes a service's class in a bot of one process, and its name with process sharding.
#311
bcda23dThanks @l7aromeo! -meocord createwrites any app name intomeocord.config.tsas a valid string. A name with an apostrophe, such as"Bob's Bot", or one ending in a backslash made a config that failed to parse, so the new app never built. A backslash elsewhere changed the name silently:Back\slashwas read asBackslash. The name is now quoted and escaped the way the app's own Prettier config writes it, so the file also passes the app's lint unchanged. An app created with such a name before this needs itsappNamefixed by hand.#306
d04a276Thanks @l7aromeo! -meocord createkeeps the app when git can't make its first commit. Before, a git failure deleted the app it had just written and exited with an error. That happened on a machine where git has nouser.email(many fresh Linux machines, containers and CI runners) and on one without git. Nowcreatefinishes, says what happened, and tells you how to finish the commit. Inside an existing Git repository it makes no new one and leaves the files to that repository.The first commit now comes after the install, so your lockfile (
bun.lock,package-lock.json,yarn.lockorpnpm-lock.yaml) is part of it, rather than showing as an untracked file in a new app's firstgit status.#389
6ac2e26Thanks @l7aromeo! -meocord create --use-npmmakes an app that npm 11.16 and later install without theallow-scriptswarning. Those versions list every dependency install script yourpackage.jsonneither allows nor denies, and a new app listed@swc/core,unrs-resolverand, on macOS,fsevents. An app created for npm now denies all three underallowScripts:@swc/coreandunrs-resolverload the native binding npm installs for your platform. Their scripts check that binding and, only where it fails to load, fetch a fallback:@swc/core's installs@swc/wasm, which@swc/coreitself does not load, andunrs-resolver's downloads the binding npm installs anyway.fseventsships its binary prebuilt. Its script rebuilds it from source, which fails because the package has no build files, so npm left the optionalfseventsout. Denied, it is installed with its prebuilt binary.
npm before 11.16 ignores the field. An app created with pnpm, yarn or bun gets no
allowScripts.An app created for npm before this gets the same by adding to its
package.json:JSON "allowScripts": { "@swc/core": false, "fsevents": false, "unrs-resolver": false }#375
8c212aeThanks @l7aromeo! -meocord create --use-pnpmmakes an app that installs and passes its own checks on pnpm, 10 and later.- pnpm 11 and later refuse to install while a dependency's build script is neither allowed nor denied, so
createstopped at "Failed to install dependencies" withERR_PNPM_IGNORED_BUILDSfor@swc/coreandunrs-resolver. An app created for pnpm now has apnpm-workspace.yamlthat leaves both scripts off underallowBuilds: each only checks the native binding pnpm installs for your platform, and fetches a fallback without it. The same file lets pnpm install themeocordthat created the app, which pnpm 11 and later would otherwise hold back for a day after its release (minimumReleaseAgeExclude). An app created with npm, yarn or bun gets no such file. - The app declares
reflect-metadataand@types/node, which its test setup imports and its tsconfig names. npm and bun hoist them from other packages, but pnpm links only whatpackage.jsondeclares, so on pnpm 10 the app'slintandtestfailed.
An app created for pnpm before this gets the same fix by adding both packages to its
devDependencies, and on pnpm 11 or later thispnpm-workspace.yaml:YAML allowBuilds: '@swc/core': false unrs-resolver: false minimumReleaseAgeExclude: - meocord- pnpm 11 and later refuse to install while a dependency's build script is neither allowed nor denied, so
#397
e1168b9Thanks @l7aromeo! -@Defer()written below@MessageHandler,@ReactionHandler,@Onor@Autocompleteis refused in one line atcreate(), as it is when written above one, rather than printed with a stack trace.#343
87dbef1Thanks @l7aromeo! -meocord start --devrestarts the bot through the bot's own stop, the one SIGINT and SIGTERM run, on every platform. On Windows, a restart used to end the bot outright, so itsonShutdownhooks never ran on a save, and whatever they release or flush was left as it was. The dev runner now asks the bot to stop over the channel it already gives it, and the bot shuts down as it does on Ctrl+C, before the new build starts. A bot that doesn't exit within itsshutdownTimeoutand a short grace period is still killed, as before. Rebuild to pick this up.When watch mode can't start, it stops a bot it had already started the same way, and exits once that bot has. Before, a bot that held on to its stop signal could outlive the CLI.
One save no longer restarts the bot twice. An editor's save can produce two builds of the same output, and a bot already running the latest output is now left alone. A change to
meocord.config.ts,tsconfig.jsonor.envstill always restarts it.#337
8504f45Thanks @l7aromeo! -meocord start --devkeeps watching when the bot cannot log in, such as with a wrong token or an intent Discord refuses. It says so, and starts the bot again on the next change, whether to your code or to.env.start --devalso watches.env,.env.local,.env.developmentand.env.development.local: saving one restarts the bot with the values the files now hold, without a rebuild. The bot reads the files itself as it starts, and inherits only what your shell set.When watch mode cannot start, for example because an
rsbuildhook inmeocord.config.tsthrows,start --devexits with code 1, asmeocord builddoes, so a script or process manager around it sees the failure.A rebuild that can't start partway through a session, such as when you save a
meocord.config.tswhosersbuildhook or plugin throws, no longer endsstart --devwith the bot still running in the background. It says the rebuild failed and why, keeps the bot and its last build running, and tries again when you save the file again.#350
b36cba3Thanks @l7aromeo! - The startup check now names a@MessageHandlerwithscope: 'dm'that can never receive a DM. A DM reaches the bot only with theDirectMessagesintent, and only with discord.js'sPartials.Channel, since no DM channel is cached after the bot starts. The generated app's client options have neither, so a DM-only command added to it never ran, and nothing said why. The warning names the handler and what is missing. A command for servers, or for both, gets no new warning.#372
270d315Thanks @l7aromeo! - The direct messagemessages.dmOnErrorsends for a command in a server now says what went wrong.meocord.dm.errorreads{command} in {channel} on {server}: {reason}, where{reason}is what the fallback answers the error with: "An error occurred while executing the command." for a fault, and "Cooldowns can't be checked right now: try again shortly." when the cooldown store is down. It said "Something went wrong running {command} in {channel} on {server}. Try again later." for both, so a store outage read as a broken command. The text has the same shape asmeocord.dm.cooldownbeside it. A catalog that translatesmeocord.dm.erroradds{reason}to it.#355
d642b0cThanks @l7aromeo! - A reaction in a DM the bot has not cached since it started reaches its handlers again. From discord.js 14.26.2, discord.js makes a channel it has not cached from a gateway event only when the event says the channel is a DM, and a reaction's event names its channel by id alone, so such reactions were dropped before any listener saw them, even withPartials.Channel. With theDirectMessageReactionsintent, MeoCord now fetches that DM channel once, on its first reaction, and hands the reaction back to discord.js, which delivers it to@ReactionHandlerand to your ownmessageReactionAddandmessageReactionRemovelisteners alike. Later reactions in that DM need no channel fetch, though the first reaction on a message the bot doesn't hold still fetches that message once, and a reaction discord.js delivers itself is left alone. One window remains: a DM reaction that arrives before the gateway is ready, in the seconds while discord.js waits for the bot's servers, is replayed by discord.js later without a raw event, so it is still dropped.#332
bb22d53Thanks @l7aromeo! - On Windows,meocord generatein a project installed with npm, yarn or pnpm reported "Failed to create" and exited 1 for every file, though the file was written. Its formatting step spawnednode_modules/.bin/eslint.cmd, which Node refuses to start without a shell. The project's ESLint now runs through the same runtime as the CLI. A formatting failure never marks a written file as failed.#357
cfb94c8Thanks @l7aromeo! -meocord generateformats the files it writes with your project's ESLint in one run, and says so. Before, it started a separate ESLint for each file, all at once. Each built your project's type information, and the command sat silent until the slowest finished: about 2.3 times the CPU for a controller with its spec and builder, enough to stallgenerateon a busy machine. Now it prints "Formatting with your project's ESLint..." after the files are created and waits for that one run. If ESLint can't run, or reports problems it can't fix,generatesays so, and the files stay as written either way.#332
c95beabThanks @l7aromeo! -meocord generate controllerwrites a spec that tests the handler. It invokes the handler with a mock of the interaction, message or reaction it handles, and checks what it answers: how it answers, such as a reply or an update of the message, and the text, such as "Hello from /ping!" or, for a mentionable select menu given a user and a role, "Selected 1 user(s) and 1 role(s)." Before, every spec only checked that the controller existed, and passed whatever the handler did. A generated slash, context menu or entry point builder now takes the command's name from@Command(build(commandName)), so the two can't drift apart. Files you've already generated are unchanged.The sample specs
meocord createwrites check the same way. The slash, context menu, button and modal samples check the text they answer. The message and reaction samples send a message or a reaction throughmodule.dispatch(), as the bot routes it, and check the reply, where before they only checked that the controller existed.- Breaking
#400
dcc5087Thanks @l7aromeo! -MeoCordFactory.create()and the testing module'scompile()now warn about each@MessageHandler,@ReactionHandler,@Commandor@Autocompleteon a class that is not one of the app's@MeoCord({ controllers }), such as a service or a class one injects. MeoCord dispatches only to controllers, so these handlers never run, and nothing said so. Move them to a controller: the next major version (5.0) refuses to start with them, as the upgrade guide describes. A sharded bot warns once, from its manager. The warnings about missing intents and partials no longer name these handlers, since no intent would make them run.@Onand@Oncehandlers run on any bound class, as before. #300
a4afcf9Thanks @l7aromeo! - A handler may return a value.@Command,@MessageHandler,@ReactionHandlerand@Autocompleteaccepted only a method returning nothing, soreturn interaction.reply(…)orreturn message.reply(…), as discord.js code often ends a handler, failed to compile with "Unable to resolve signature of method decorator". As with@On, any return type compiles: MeoCord answers nothing with it, and an interceptor receives it fromnext.handle(). A parameter of the wrong type is still refused.#397
622010cThanks @l7aromeo! - The built-in help, and the list of subcommands a parent command with no handler of its own answers, now run the app's@MeoCord({ guards })first, as a command does. A guard that returnsfalseleaves the message unanswered, and one that throwsGuardDeniedErrorgets its reason as the reply, as a denied command does. They answered whatever the app's guards decide, so a bot its guards close in a channel, or to a user, still answered help there.- Breaking
#334
4de3194Thanks @l7aromeo! - A handler that a subclass re-declares follows one rule for@Command,@MessageHandler,@ReactionHandlerand@Autocomplete.- On the route it inherits, the subclass's declaration takes that route's place, so the subclass's options apply: a re-declared
@Command('ping', LoudPingBuilder),@MessageHandler('roll', { description })or@ReactionHandler('👍', { bots: true })uses its own builder, description or settings. In 4.0 the base's applied, since the base's declaration came first. To keep the base's builder or options, don't re-declare that route on the subclass, or declare it with the base's builder. The routes the subclass answers are unchanged. - On another route, the subclass still answers the route it inherits too, as in 4.0. For example, a subclass overrides
page(), which its base declares as@Command('page/{n}', …), with@Command('shop/page/{n}', …), and answers both. The bot now names each such handler in one warning as it starts, with the routes it inherits and its own. In the next major version (5.0), a handler's own routes replace the ones it inherits. To keep an inherited route, declare it on the subclass's method as well.
A class between them that declares nothing changes neither. A subclass that declares every route itself, or none, gets no warning. See the upgrade guide.
- On the route it inherits, the subclass's declaration takes that route's place, so the subclass's options apply: a re-declared
#341
e09926bThanks @l7aromeo! - An interceptor that callsnext.handle()without returning or awaiting it no longer crashes the bot when the handler throws. The handler's error was an unhandled rejection, which ends the process, and the filters, the fallback and the user never saw it, while observers reported the call as'ran'and@Defer()released the message before the handler finished. Now, when an interceptor returns and leaves whatnext.handle()returns, or athenorfinallychain from it, without a rejection handler, asnext.handle().then(log)does, the call ends when the handler does and fails with what it throws: its filters and the fallback answer it as they would any handler error. An interceptor that awaits, returns or catches the promise, or races it against a timeout, behaves as before. A promise handed to something else, such asPromise.all, is still that one's to handle.A handler that an interceptor took on, such as by racing it against a timeout, and that throws after the interceptor has returned is now logged as a warning naming the interceptor, the handler and the error, unless a handler of the interceptor's own, in a chain from
next.handle(), disposes of it; one that rethrows it, or wraps it in an error of the app's, still leaves it to the warning. The call has ended by then, so nothing else reports it.#328
dffb24dThanks @l7aromeo! - The editor documentation of three APIs says more about what they do:TestingModule.invokesays that a guard'sGuardDeniedError, aUserErrorand aCooldownErrorreject it, wheredispatchresolves{ ran, error }, and its example shows both.useThemenamesthemeFor's layers: the server's theme, then the user's, over the handler's@UseTheme.createMocksays that a property its type declares as data is a mock function, so truthy, and shows passing the values the code reads.
- Breaking
#282
1f8fbf4Thanks @l7aromeo! -Loggerprints asconsole.logdoes. It redacts the bot's credentials from everything it prints, as 4.0.1 does.- Colour follows each line's stream. Warnings and errors go to stderr and the other levels to stdout, but colour followed stdout alone, so
node dist/main.js 2>>errors.logfrom a terminal wrote colour codes into the file. Each line now takes colour only where its own stream is a terminal, orFORCE_COLORasks for it. - An object is coloured only where the rest of its line is. An object or other non-string argument was always printed with colour codes, even into a file or a log collector, where they appear as raw escape sequences. It now prints in colour on a terminal, and plain where the output is not one. Set
FORCE_COLOR=1to keep colour where your log viewer shows it. - Objects print as
console.logprints them. A small object such as{ id, name }takes one line, where 4.0 printed one property per line. Objects print four levels deep, and without their non-enumerable properties, as 4.0.1 does, where 4.0.0 and the earlier 4.1 betas printed every level and those properties: nested data a bot logs, such as a payload or its settings, still shows in full, and a discord.js structure prints a few hundred lines instead of everything it reaches. An error prints its stack, its own properties such ascode, itscause, and anAggregateError's errors, with its message and stack printed once, as in 4.0.1. - A value of any type prints, and none makes
Loggerthrow. ASymbolthrewCannot convert a Symbol value to a string, so a handler or an observer that threw aSymbolmade MeoCord's own log of it throw too, and from an observer that became an unhandled rejection. Anything other than a string now prints asconsole.logprints it:Symbol(boom),10n,[Function: handler]. A number or a boolean takesconsole.log's colour rather than the level's. logger.info()andlogger.verbose()lines are tagged[INFO]and[VERBOSE]. 4.0 tagged them[LOG]. They still print at theloglevel, so a filter or parser that matches[LOG]needs to match[INFO]and[VERBOSE]too to keep catching them. See the upgrade guide.
- Colour follows each line's stream. Warnings and errors go to stderr and the other levels to stdout, but colour followed stdout alone, so
#322
1ceb803Thanks @l7aromeo! - A@MessageHandler(pattern)handler whose params don't fit its pattern is now explained in three lines rather than twenty-four. The decorator had one form for each way a handler can be declared, so TypeScript explained the mismatch against every form, and the reason, such as a param the pattern lacks, came last. It now has one form, and the second line names the params that don't fit and what the pattern gives each:"The handler's params do not fit the pattern": { side: { readonly 'not a param of the pattern': "side" } }. What compiles is unchanged.#304
31f0f02Thanks @l7aromeo! - Two message handlers that can take the same message stop the bot at startup, wherever their starts are known as it starts. One with its ownprefix: '!'and one using the app's'!', own prefixes that share one ('!'and['!', '?']),prefix: falsebeside an app with no prefix, or two a mention starts in a server were all taken as different starts, so the order of yourcontrollersdecided which ran. The refusal names both handlers and their patterns; give one another prefix or pattern. An app whose prefix is a function gives its prefixes only as each message arrives, so a handler using it is refused only beside another that uses it too. Beside one with its own prefix orprefix: false, which the function can also give, the handler with its own start runs, whatever the order of yourcontrollers.A prefix function that finds no prefix for a message, returning an empty list,
undefinedornull, now lets no prefix start a command for it; a mention still does whenmentionis on. It took the message as it is, so in a server with no prefix of its own, plain chat starting with a command's word ran that command. Return''to take a message as it is. The function's type takesundefinedandnulltoo, so a lookup such asreturn prefixes.get(id)needs no cast.#347
928aa57Thanks @l7aromeo! - Withmessages.dmOnErroron, a message command refused because the cooldown store is down now DMs its author, once per outage, themeocord.dm.errormessage, ormeocord.cooldown.storeDownfor a command sent in a DM. Such a command got no answer at all, so while the store was down every!commandwith a cooldown looked like a dead bot. WithoutdmOnErrorit is still skipped silently, as before.#370
0ff8ebaThanks @l7aromeo! -meocord/testing's mocks take where they were made, and what a user option carries, as discord.js reads them:- A channel given to
createMockInteractionorcreateMockMessagesets thechannelId,guildIdandguildthe test leaves out. A DM channel is no server, soinGuild()isfalse, and a server's channel puts the mock in its server. An interaction given a channel kept achannelIdof its own, so a per-channel cooldown counted each one apart, and a message given a DM channel said it was in a server. A server's channel that names no server is put in the mock's, and a channel in another server than the one the test gives, or a DM's where it gives a server, is refused with both named. The channel is cached on the mock's client, as the gateway caches it. - A mock channel's managers have it as their
channel, and a thread'smembersas theirthread, as discord.js's do. - A user option from
createChatInputOptionscarries itsuser, and in a server itsmember, as the gateway sends them. A member given carried onlymember, as in 4.0, so a handler's param typedUsergot theGuildMember. - A select menu given its
users,members,rolesorchannelshas their ids asvalues, as Discord sends them. They stayed empty unless given as well.
- A channel given to
#333
60e0290Thanks @l7aromeo! - A mock select menu fromcreateMockInteractionhas picked nothing unless the test gives its choices, as discord.js builds one:valuesis an empty array, andusersandmembers,rolesorchannelsare emptyCollections, the ones its kind picks. They were stubs, so a handler'sinteraction.users.map(...)orinteraction.values.lengththrew or read a function on a default mock. Values and collections a test gives are kept.#374
78d444eThanks @l7aromeo! - A modal Discord refuses as already acknowledged (40060) now leaves the interaction answered, as a refused reply or update does, so the nextrespond().send()edits the answer that was made elsewhere instead of failing the same way.modal()still rejects with the refusal.#397
13d8521Thanks @l7aromeo! - A modal's file upload field reaches the handler's params as an array of the uploadedAttachments, the onesinteraction.fields.getUploadedFiles()gives. It gave the attachments' ids. For a bot upgrading from an earlier 4.1 beta, a handler that read ids from the field takes them from the attachments:files.map(file => file.id).createModalFieldstakes an array ofAttachments for a file upload field, so a test can submit one:createModalFields({ screenshot: [attachment] }).#358
8773cf8Thanks @l7aromeo! - A presenter that fails to draw a view no longer leaves the user without an answer:- An error view whose
error()throws or rejects, or returns a view MeoCord cannot render, such as a colour that is no colour or an empty text, is answered with MeoCord's own error view. The failure is still logged as the call's fault, and a testing module'sdispatchstill rejects with it. An asynchronouserror()was the only one answered this way before. - A loading view whose
loading()fails the same way, or takes longer than a second to draw, is replaced by MeoCord's own loading view, with a warning naming the presenter, so the click is still locked and the handler still runs. A drawing that comes later is left unused.
- An error view whose
#342
d9a03d0Thanks @l7aromeo! - Three fixes to how@MeoCord({ providers })and a testing module wire classes and providers:- A provider that would receive
ExecutionContextis refused as the app is created: a factory provider whoseinjectlists it, or auseClassprovider whose class injects it. The message names where it is declared and its token, such asApp: @MeoCord({ providers }): the provider for 'audit' uses Audit, which injects ExecutionContext, …. Such a provider is made once, so the context it received was an empty one, bound for the whole app, and every later call shared it. - A class whose own source contains the text
[native code], such as one that inspects functions, is injected like any other class of the app. It was taken for a built-in constructor and never bound, so the app stopped at startup withNo bindings found for service. - Providers or classes that inject each other in a cycle are refused as the app is created, naming the cycle, such as
'a' → 'b' → 'a'. The app failed when the first of them was made, withCircular dependency found: (No dependency trace).
- A provider that would receive
#353
bbe1ef2Thanks @l7aromeo! - A reaction no longer re-fetches its message from Discord's API when the bot already holds it whole. Every reaction add and remove cost one request, queued behind Discord's rate limits, so a reaction-role or poll message delayed every handler under a burst of reactions. Nowreaction.messageis the copy the gateway keeps current, fetched first only when the bot holds the message by its id alone, and a reaction that arrives without its count (withPartials.Reaction) is fetched once, soreaction.countis no longernull.This changes 4.0's behaviour, which fetched the message for every reaction. The cached copy differs from a fresh fetch only rarely: after a reconnect that could not resume and so missed an edit, or for a poll's counts when the bot lacks the
GuildMessagePollsintent. A handler that needs the message straight from Discord callsawait reaction.message.fetch()itself.#397
fe021d7Thanks @l7aromeo! -HandlerRegistry.messageHelp()reads the message commands from the table dispatch routes with, so a help command of the app's own lists exactly what the built-in help lists. It also read@MessageHandlers on services, which no message reaches, and listed or described them as commands.#288
6e62061Thanks @l7aromeo! -respond()makes the answers of one interaction one after another, and keeps its state right when Discord refuses one.- Answers asked for together, such as two
send()calls at once or the fallback's error while asend()is in flight, run in the order they were made: the first replies and the next edits or follows up, instead of both replying. - An acknowledgement that fails leaves the interaction unanswered, so the next
send()replies instead of throwing that failure again. - A reply or an update Discord refuses as already acknowledged still throws, and the next
send()edits. The error answer MeoCord follows up with after such a refusal now reaches Discord. send(),edit()anddelete()aftermodal()throw an error saying to answer the modal's submit, since a modal has no message.modal()also throws while another answer is in flight.- In
meocord/testing, a mock command that showed a modal rejectseditReply(),fetchReply()anddeleteReply()withUnknown Message(10008).
- Answers asked for together, such as two
- Breaking
#335
e524937Thanks @l7aromeo! - Routing fixes for handlers that compete for the same interaction:- Two
@Autocompletehandlers for one option are named at startup. Two handlers that complete the same option of a command, or every option of one path, used to start silently, and the first controller listed always won. The warning about command handlers that never run now names the one that never does, and the one that runs instead. Handlers of an option Discord never asks to complete are named for that alone. The bot still starts. In the next major version (5.0), it refuses to start. See the upgrade guide. - The warning about overlapping component patterns names the handler that runs. For each pair of patterns that can match the same customId, such as
a/{x}/canda/b/{y}, it now says which handler runs for the ids both match, and why: the more specific pattern, or between equally specific ones, the one whose controller is listed (or handler declared) first, as in 4.0. Where the next major version (5.0) runs the other one instead, preferring the pattern that spells out the first segment where the two differ, the warning says so, and what to do so the bot does the same before and after: list that handler's controller (or declare that handler) first, or make the patterns distinct. See the upgrade guide. - The overlap warning is given once, as the bot starts. It is a startup check like the others:
meocord start,meocord registerand the shard manager give it once, where each process-sharded shard gave it again, andMeoCordTestingModule.compile()gives it without anything being dispatched. Two handlers whose patterns match exactly the same customIds are refused there too: by the shard manager before it spawns a shard, and byMeoCordTestingModule.compile(). A test that expectedinvoke()to reject such a module now seescompile()throw the same error. @Command's documentation now says that only patterns matching exactly the same ids stop the bot, that overlapping ones are warned about, and how the next major version (5.0) breaks a tie.@Autocomplete's names the two startup warnings about its handlers.
- Two
#396
5ae9cc1Thanks @l7aromeo! - Fixes at the edges of shutdown, sharding, themes and the handler registry:- With
sharding.development, a Ctrl+C whilemeocord start --devrestarts the bot joins that stop, as it does in one process; it no longer kills every shard and exits 1. - The shard manager's
stop()setsprocess.exitCodeto 1 when it has to kill a shard, unless another code is set, as a bot in one process does when its client fails to close. - A shard's
stop(), and its report of a failed start, wait at most a second for a manager that is gone, where Bun would otherwise wait for ever. - An
onShutdownhook does not run for a class whoseonReadywas still running when shutdown began, even when it finishes while the calls under way are waited for, as theOnShutdowndocs say. useTheme()outside a call reads the app's theme until the app has shut down, soonShutdownhooks and the calls shutdown waits for read it too.- A
themeForlookup thatThemeCacheforgot while it was in flight logs nothing when it fails. HandlerRegistrygives an entry point command, whose builder returns a REST body, itscommandanddescription. A handler without a builder of its own gets the JSON of its own kind of command, by type and name as Discord tells commands apart, and a context menu's by its whole name. A message command'sscopeis narrowed to'guild'by amember,roleorchannelparam or flag, as help shows it.- A context menu whose name has a space and whose builder cannot be serialised is no longer reported as one no builder registers.
- A localization under a key that is not a Discord locale is reported once, as an unknown locale, and its value is not checked.
- With
#284
204f93fThanks @l7aromeo! - Message command params, flags and customId segments look up param types and booleans in tables with no inherited keys, so a customId param named like an inherited key, such as{__proto__:int}, keeps its type. A word such asconstructorortoStringis now an invalidboolvalue, answered with the usage like any other wrong word. A{name:type}whose type is such a name is refused at startup as naming no type, whether or not the app adds its ownmessages.types. An app type is matched only by a name the app gave it.route().build()reads a param's value and type by the param's own name, so a param namedconstructorortoStringbuilds from the value given, and asks for one when none is.#327
47069bcThanks @l7aromeo! -ShardContext.callfixes:- Identical concurrent calls run once each. With process sharding, two identical calls made at once, such as two users triggering the same announcement, used to run once in each shard and share one answer, and a later identical call could get an earlier call's answer. Each call now runs in every shard and gets its own answer.
- One process and tests now pass values as JSON, as process sharding does. In one process, which is how
meocord start --devand the testing module run, the arguments and the result were passed as live objects, while process sharding sends them as JSON. ADatearrived as aDatein development and tests, then as a string in production, and a returnedMaparrived as{}. They're now passed through JSON in every mode, so a test sees what production gets. - A class a provider stands in for can be called.
call(Payments, 'charge')withproviders: [{ provide: Payments, useClass: StripePayments }]answered "Payments is not a controller or service of this app." It now runsStripePayments.charge. With process sharding, a call from another shard names the class, so the bot refuses to start when a provided class shares a name with a controller, a service or another provided class, as it already did for two controllers or services.
#380
e0631abThanks @l7aromeo! - The process-sharding manager checks what it can before it does anything:- A build for another platform stops before the manager registers commands or spawns a shard. The check ran only in each shard, so the manager registered the commands and spawned shard 0 before the build was refused.
- A missing bundle is found before anything is registered. A manager started without a bundle to spawn shards from registered the commands and asked Discord for the shard count before saying it could not find one.
start()starts the manager once. A second call, at once or later, registered the commands and spawned every shard again; it now waits for the first.
#321
22e3c3eThanks @l7aromeo! - Process sharding now handles a shard whose start fails:- A failed start ends the shard, so its manager restarts it. The failure might be a network error at login, a provider factory that rejects, or Discord being briefly unavailable. Before, the shard set exit code 1 and kept running without logging in, so the manager never restarted it and its servers stayed offline until the whole bot was restarted. The shard now exits 1 once
main.tshas handled the rejection, and the manager restarts it with the usual backoff. - An app MeoCord refuses stops the bot. Examples are two services with one name, a provider of the wrong shape, or native addons built for another platform. Every shard would refuse it alike, so the shard tells its manager, which logs "Shard N cannot start; stopping every shard." with the reason, and where it is, on lines of its own, stops every shard and exits 1. Under
meocord start --dev, the session then reports that the application exited with code 1 and waits for a change, rather than that the bot could not log in. Before, the manager restarted the shard about once a minute, forever, while a process supervisor saw a healthy process. - A shard listens to its manager from the start. A stop request, or the manager going away, while the shard's providers are still being made now stops the shard before it logs in. Before, the shard went on to log in with no manager, and a restarted manager then ran a second copy of it.
Outside process sharding, a failed
start()still leaves the process tomain.ts.- A failed start ends the shard, so its manager restarts it. The failure might be a network error at login, a provider factory that rejects, or Discord being briefly unavailable. Before, the shard set exit code 1 and kept running without logging in, so the manager never restarted it and its servers stayed offline until the whole bot was restarted. The shard now exits 1 once
#351
2ce5380Thanks @l7aromeo! - A shard whose manager is gone no longer crashes on a call with a cooldown. When the manager died, each shard began its graceful shutdown, butShardedCooldownStorestill sent to the closed IPC channel. On Node, that send raised an unhandled'error'event, so the shard exited 1 at once and skipped its shutdown hooks. On Bun, the message was dropped, and the call waited outcooldownStoreTimeoutMs. Now the store checks the channel before sending and reports a failed delivery to the call, so the call fails withCooldownStoreErrorat once on both runtimes, and the shutdown carries on. A call sent just before the manager died fails as soon as the channel closes, too, rather than waiting outcooldownStoreTimeoutMs.#369
951d5acThanks @l7aromeo! -shutdownTimeoutinmeocord.config.tsis at most 2147478647 ms. Node fires a timer longer than 2147483647 ms at once, and the shard manager andmeocord start --devwait up to 5 seconds pastshutdownTimeout, so a larger value made the bot or those two give up on shutdown at once. The config now refuses such a value as it loads, as it refuses a negative one. A bot started without the CLI, whose config is not checked, waits the default 10 seconds instead and warns, naming the value it was given.@MeoCord({ themeForTimeoutMs })namesInfinityorNaNin its refusal, where it saidnull, and words it ascooldownStoreTimeoutMsdoes.- Breaking
#315
f12857dThanks @l7aromeo! - A@Catchgiven something that is not an error class, such as anundefinedfrom an import cycle, is named in a warning as the filter loads, and now matches no error:Broken: @Catch's first entry, undefined, which matches no error, is deprecated; in the next major version (5.0) it is refused. Use an error class, such as @Catch(CooldownError), instead.It used to throw "Right-hand side of 'instanceof' is not an object" at the first error to reach the filter, which hid the handler's own error and handled the call a second time. The bot still starts; in the next major version (5.0) it is refused. See the upgrade guide. #336
1e442d1Thanks @l7aromeo! - A base controller's class stages now wrap the classes that extend it, as global stages wrap controllers. Guards and interceptors run the top base's first, then each subclass's, then the method's. Filters are tried the other way: the method's, then the subclass's, then each base's, then the global ones. In the 4.1 betas the subclass's guards ran before the base's, so a guard a subclass added ran even for callers the base's auth guard would refuse. For a handler the subclass declared itself, the base's filters were also tried first, so a catch-all on the base hid the subclass's own, more specific filter. Class cooldowns keep their order, andinspectHandlerand theMetadataKey.Guardsmetadata list the stages in the order they now run.- Breaking
#287
dea37a4Thanks @l7aromeo! -start()sets the bot up once. Calling it again after a failed login used to attach every event handler a second time, so each command, message and reaction ran twice,onReadyran twice, and an app with a presenter could no longer answer the built-inhelp. Now a retry logs in again with the handlers it already has. Two calls at once share one start, and a call once the bot is online does nothing.After a retry logs in, the client works as a fresh one would:
isReady()reports it, shutdown closes the gateway, and configured cache sweepers run again. A failed login makes discord.js destroy its client, so MeoCord undoes that before trying again.A retry that logs in now clears the exit code the failed login set to
0, which Bun keeps, rather than toundefined, which Bun ignores. It also gives code that runs outside a handler, such as a scheduled job callinguseTheme(), the app's theme again, which the failed login had dropped.Retrying
start()after a failed login is deprecated; in the next major version (5.0) it rejects. UseMeoCordFactory.createto make a new app instead. It logs that warning once. A retry after a provider's factory failed stays supported, with no warning. See the upgrade guide. #379
d5a2e03Thanks @l7aromeo! - A class that injectsCooldownStoregets the app's store, and nothing else is made of it.- The store's hooks run once. A service that injected
CooldownStorebeside@MeoCord({ cooldownStore })made the token count as a class of its own, so the store'sonReadyandonShutdowneach ran twice, and the firstonShutdowncame before the calls under way had finished. The token now stands for the app's store, so the service depends on the store, and the store's hooks run once, after the last call. MeoCordTestingModulebinds the app's store first, as the bot does. A module made withfromApp(), or withapp, whose app has acooldownStore, failed to compile with "Ambiguous bindings found for service: CooldownStore" when one of its classes injected the token.- A cycle through the token is named. A store that injects a class which injects
CooldownStoreis refused where it is declared, naming the classes, as any other cycle is, rather than failing with inversify's "Circular dependency" as the bot starts.
- The store's hooks run once. A service that injected
#330
c62618fThanks @l7aromeo! -MeoCordTestingModule.create({ app })counts cooldowns in the app'scooldownStore, asfromAppand the bot do. It counted them in a store of its own in memory, so a test of a cooldown never reached the app's store. ACooldownStorein the module'sprovidersstill takes the app's place, and the app's own store is then never built, so what it injects needs no provider. A store of the app's that injects something the test doesn't list needs it inproviders, or aCooldownStoreprovider in its place.#392
421081bThanks @l7aromeo! - The documentation your editor shows formeocord/testingandmeocord/decoratornow matches what the code does. Nothing to change in your code.- Mocks:
createMockClientsays only a mock message gets a client of its own; give an interaction itsclientwhen the code under test reaches it.createMockGuildsays what a manager'sfetch(id)makes: a member or channel in the guild, a role with that id, or a ban.createMockUseris for a user, andMockPropsnames only a property the mock does not let you assign.createMock,createMockGuildandcreateChatInputOptionsdocument their parameter. - Testing module: the
invoke,themeCacheandoverrideThemeForexamples compile, andexpectCompleteCatalog's example passes.observersandinit()namedispatchbesideinvokeandemit.inspectHandler'sappsays the app's filters are tried after the handler's own.resolveRoute'sdmsays when a handler outside its scope is returned. AtestCooldownStorecase title claims only what it checks. - Decorators:
@Cooldown,@ValidateandcooldownStoreFailuresay how a message command over its limit, with invalid input or with the store down is answered.@Deferdescribesmode: 'auto'and when a misplaced@Deferis refused.@Servicesays when to list a class inservices.@Controllersays which handlers its class stages reach.@Commandlists everything it refuses as it applies, and@Autocompletesays a handler's own guards run.@Guard,@Interceptor,@Observerand@Validatedocument their options.
- Mocks:
#398
14f6d3cThanks @l7aromeo! -meocord/testingmocks andresolveRoutebehave as the bot does in four more places:- A mock interaction has a client.
createMockInteractiongives an interaction made without aclientone fromcreateMockClient, as a mock message has, with the interaction's user inclient.users.cacheand its channel inclient.channels.cacheonce read. Code that reachesinteraction.client, such asinteraction.client.users.cacheorinteraction.client.user.id, reads real caches and the mock bot's id rather than stubs. getAttachment()returns an attachment option.createChatInputOptions({ file })with anAttachmentmakesoptions.getAttachment('file')return it, andnullfor an option not given.- Resolved media never takes the bot's id. A thumbnail, image or media gallery item an edit resolves gets an id no other mock has, the mock bot's included.
resolveRoute(app, { content, dm: true })returns nothing for a handler that works only in a server, by itsscopeor amember,roleorchannelparam in its pattern, as dispatch answers such a message with its usage and never runs the handler.
- A mock interaction has a client.
#289
1659e78Thanks @l7aromeo! - A build resolves yourpathsfromcompilerOptions.baseUrlwhen yourtsconfig.jsonsets it, as TypeScript does. Before, the build read everypathstarget from the project root, so an alias such as"@lib/*": ["lib/*"]with"baseUrl": "./src"typechecked but failed to resolve inmeocord buildandmeocord start --dev. AbaseUrlyourtsconfig.jsononly inherits throughextendsisn't applied to thepathsit sets itself, so declare thosepathsrelative to your project's owntsconfig.json. TypeScript 6, which a new app pins, deprecatesbaseUrl, andtscstops with TS5101 where it is set. WithoutbaseUrl, TypeScript and the build both readpathsfrom the foldertsconfig.jsonis in, as a new app's"@src/*": ["./src/*"]does, so write each target from there, or keepbaseUrland set"ignoreDeprecations": "6.0".meocord start --devrebuilds when you savemeocord.config.tsortsconfig.json, and each rebuild writes a copy of yourtsconfig.json. Those copies now share one temporary directory and one exit listener for the session, instead of one each. On Node, a session with several such saves no longer printsMaxListenersExceededWarningforexit.A build or
start --devthat stops before it can clean up, such as one killed, crashed or closed with its terminal, left its directory in your system's temp directory for good. The next build orstart --devon the same machine now removes it. Directories left by earlier 4.1 betas, namedmeocord-tsconfig-and six characters, can't be told apart from a build that is still running, so they stay: delete them yourself while no MeoCord build orstart --devis running.#338
b6a34afThanks @l7aromeo! -warnUnanswerednow also warns when an interceptor returns without callingnext.handle()and leaves the interaction unanswered, or deferred by@Defer()with no follow-up, as an interceptor that answers from a cache can. The warning names the interceptor:Shop.buy: its interceptor Cached returned before the handler ran, without answering the interaction, …. It warned only when the handler itself ran, so this case passed silently while the user saw "The application did not respond".An interceptor that returns before the handler finishes, as one racing
next.handle()against a timeout does, is named the same way, the outermost when several do,… returned before the handler finished, …, rather than the warning blaming the handler for an answer it was still about to send.- Breaking
#323
ef58d97Thanks @l7aromeo! - The startup warning about command handlers that Discord never sends now also covers@Autocompletehandlers. One is named when:- no builder registers its command;
- its path isn't a subcommand that its command's builder registers;
- it names an option the builder doesn't register, or one built without
setAutocomplete(true); - it completes every option of a subcommand that has no option with autocomplete on.
A handler of the whole command is checked against every option of the command, its subcommands' included, since MeoCord falls back to it for all of them. The bot still starts. In the next major version (5.0), these refuse to start. See the upgrade guide.
MeoCordTestingModule.compile()names the same cases, apart from a command no builder registers, as it already does for@Commandhandlers.meocord generate controller autocomplete <name>now ends by saying what to add to/<name>'s builder, thequeryoption withsetAutocomplete(true), since the generated handler completes that option and Discord never asks it to until the command declares it. - Breaking
#307
db19771Thanks @l7aromeo! - A command handler that Discord never sends an interaction to now gets a warning when the app is created, someocord start,meocord registerand the shard manager report it. Before, such a handler was dead or misrouted with no sign at startup. One warning names every such handler, what is wrong and what to do:- a subcommand path that the command's builder doesn't register:
@Command('settings notfy', CommandType.SLASH)beside asettingsbuilder withviewandnotify. Before,/settings notifysilently ran thesettingshandler; - a customId pattern, such as a
route(), given to a slash, context menu or entry point handler, which is matched by its command name; - a builder that registers another name than its
@Command's, such assetName('ping')under@Command('pong', PingBuilder); - a slash, context menu or entry point command that no builder registers at all.
The bot still starts. In the next major version (5.0), these refuse to start. See the upgrade guide.
MeoCordTestingModule.compile()gives the same warning, apart from the last case: a handler with aCommandTypeand no builder is how a test fixture is written. - a subcommand path that the command's builder doesn't register:
#339
d2fb9cfThanks @l7aromeo! - How MeoCord logs and echoes what users send:- Autocomplete and reactions log their users' outcomes at debug level, as commands do. An autocomplete call that a guard denies, or whose handler throws a
UserErroror aValidationError, still closes its menu, and is no longer logged as an error with its stack. The same holds for a guard that denies a reaction.TestingModule.dispatchresolves for such an autocomplete call or reaction, where it rejected; read what stopped it from the result'serror. - Log lines quote what a user sent on one line. A message's text, a reaction's emoji and a component's customId are quoted with line breaks, control characters and quotes escaped, and a message's text is cut short after 200 characters, with its length. So are the messages of the debug lines for a refused, denied or invalid call.
- Usage and help replies show the user's words as typed. A param's value, a flag's name or value, and the command a
helpquery names are quoted with their markdown escaped and on one line, so**up**or[text](https://example.com)reads as written instead of rendering as bold text or a link.
- Autocomplete and reactions log their users' outcomes at debug level, as commands do. An autocomplete call that a guard denies, or whose handler throws a