Skip to content

Commit 0f78cd7

Browse files
authored
Merge pull request #472 from underctrl-io/metadata
feat: command metadata
2 parents dd2da64 + 85f2ce0 commit 0f78cd7

File tree

13 files changed

+358
-40
lines changed

13 files changed

+358
-40
lines changed

apps/test-bot/src/app/commands/(developer)/server.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,17 @@ import {
33
type ChatInputCommand,
44
type MessageCommand,
55
Logger,
6+
CommandMetadata,
67
} from 'commandkit';
78

89
export const command: CommandData = {
910
name: 'server',
1011
description: 'server command',
11-
guilds: [process.env.DEV_GUILD_ID!],
12+
};
13+
14+
export const metadata: CommandMetadata = {
1215
aliases: ['s', 'serv'],
16+
guilds: [process.env.DEV_GUILD_ID!],
1317
};
1418

1519
export const chatInput: ChatInputCommand = async (ctx) => {

apps/test-bot/src/app/commands/(general)/componentsTest.tsx

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
ChatInputCommand,
33
CommandData,
4+
CommandMetadataFunction,
45
Container,
56
MessageCommand,
67
TextDisplay,
@@ -10,7 +11,12 @@ import { Colors, MessageFlags } from 'discord.js';
1011
export const command: CommandData = {
1112
name: 'components-test',
1213
description: 'Test components v2 again',
13-
aliases: ['ct'],
14+
};
15+
16+
export const generateMetadata: CommandMetadataFunction = () => {
17+
return {
18+
aliases: ['ct'],
19+
};
1420
};
1521

1622
export const chatInput: ChatInputCommand = async (ctx) => {

apps/test-bot/src/app/commands/(general)/gallery.tsx

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import {
22
ChatInputCommand,
33
CommandData,
4+
CommandMetadata,
45
MediaGallery,
56
MediaGalleryItem,
67
TextDisplay,
@@ -12,6 +13,11 @@ export const command: CommandData = {
1213
description: 'Test components v2 gallery',
1314
};
1415

16+
export const metadata: CommandMetadata = {
17+
botPermissions: ['BanMembers'],
18+
userPermissions: ['Administrator'],
19+
};
20+
1521
const mediaItems: string[] = Array.from(
1622
{
1723
length: 6,

apps/test-bot/src/events/messageCreate/01-log.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
stopEvents,
44
isErrorType,
55
CommandKitErrorCodes,
6+
Logger,
67
} from 'commandkit';
78

89
const handler: EventHandler<'messageCreate'> = (message) => {
@@ -12,15 +13,15 @@ const handler: EventHandler<'messageCreate'> = (message) => {
1213
stopEvents(); // conditionally stop the event chain
1314
} catch (error) {
1415
if (isErrorType(error, CommandKitErrorCodes.StopEvents)) {
15-
console.log('Stopping event chain');
16+
Logger.log('Stopping event chain');
1617
// if stopEvents() is called in the try block, throw it so CommandKit can stop the event chain
1718
throw error;
1819
}
1920

20-
console.log('Not stopping event chain');
21+
Logger.log('Not stopping event chain');
2122
// this means that the code threw the error, and stopEvents() was not called
2223
// the rest of the event handlers will be executed as normal
23-
console.error(error);
24+
Logger.error(error);
2425
}
2526
};
2627

apps/test-bot/src/events/messageCreate/02-log.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ import {
33
stopEvents,
44
isErrorType,
55
CommandKitErrorCodes,
6+
Logger,
67
} from 'commandkit';
78

89
const handler: EventHandler<'messageCreate'> = (message) => {
@@ -14,15 +15,15 @@ const handler: EventHandler<'messageCreate'> = (message) => {
1415
// stopEvents(); // conditionally stop the event chain
1516
} catch (error) {
1617
if (isErrorType(error, CommandKitErrorCodes.StopEvents)) {
17-
console.log('Stopping event chain');
18+
Logger.log('Stopping event chain');
1819
// if stopEvents() is called in the try block, throw it so CommandKit can stop the event chain
1920
throw error;
2021
}
2122

22-
console.log('Not stopping event chain');
23+
Logger.log('Not stopping event chain');
2324
// this means that the code threw the error, and stopEvents() was not called
2425
// the rest of the event handlers will be executed as normal
25-
console.error(error);
26+
Logger.error(error);
2627
}
2728
};
2829

packages/commandkit/src/app/handlers/AppCommandHandler.ts

Lines changed: 116 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import {
2-
ApplicationCommandType,
32
AutocompleteInteraction,
43
Awaitable,
54
Collection,
@@ -13,7 +12,11 @@ import {
1312
import type { CommandKit } from '../../commandkit';
1413
import { AsyncFunction, GenericFunction } from '../../context/async-context';
1514
import { Logger } from '../../logger/Logger';
16-
import type { CommandData } from '../../types';
15+
import type {
16+
CommandData,
17+
CommandMetadata,
18+
CommandMetadataFunction,
19+
} from '../../types';
1720
import colors from '../../utils/colors';
1821
import { COMMANDKIT_IS_DEV } from '../../utils/constants';
1922
import { CommandKitErrorCodes, isErrorType } from '../../utils/error-codes';
@@ -25,6 +28,14 @@ import { MessageCommandParser } from '../commands/MessageCommandParser';
2528
import { CommandRegistrar } from '../register/CommandRegistrar';
2629
import { Command, Middleware } from '../router';
2730
import { getConfig } from '../../config/config';
31+
import { beforeExecute, middlewareId } from '../middlewares/permissions';
32+
33+
const KNOWN_NON_HANDLER_KEYS = [
34+
'command',
35+
'generateMetadata',
36+
'metadata',
37+
'aiConfig',
38+
];
2839

2940
/**
3041
* Function type for wrapping command execution with custom logic.
@@ -38,6 +49,8 @@ export type RunCommand = <T extends AsyncFunction>(fn: T) => T;
3849
*/
3950
export interface AppCommandNative {
4051
command: CommandData | Record<string, any>;
52+
generateMetadata?: CommandMetadataFunction;
53+
metadata?: CommandMetadata;
4154
chatInput?: (ctx: Context) => Awaitable<unknown>;
4255
autocomplete?: (ctx: Context) => Awaitable<unknown>;
4356
message?: (ctx: Context) => Awaitable<unknown>;
@@ -73,8 +86,8 @@ interface AppCommandMiddleware {
7386
*/
7487
export interface LoadedCommand {
7588
command: Command;
89+
metadata: CommandMetadata;
7690
data: AppCommand;
77-
guilds?: string[];
7891
}
7992

8093
/**
@@ -129,8 +142,22 @@ const commandDataSchema = {
129142
userContextMenu: (c: unknown) => typeof c === 'function',
130143
};
131144

145+
/**
146+
* @private
147+
* @internal
148+
*/
132149
export type CommandDataSchema = typeof commandDataSchema;
150+
151+
/**
152+
* @private
153+
* @internal
154+
*/
133155
export type CommandDataSchemaKey = keyof CommandDataSchema;
156+
157+
/**
158+
* @private
159+
* @internal
160+
*/
134161
export type CommandDataSchemaValue = CommandDataSchema[CommandDataSchemaKey];
135162

136163
/**
@@ -444,8 +471,8 @@ export class AppCommandHandler {
444471

445472
if (
446473
source.guildId &&
447-
loadedCommand.guilds?.length &&
448-
!loadedCommand.guilds.includes(source.guildId!)
474+
loadedCommand.metadata?.guilds?.length &&
475+
!loadedCommand.metadata?.guilds.includes(source.guildId!)
449476
) {
450477
return null;
451478
}
@@ -499,8 +526,8 @@ export class AppCommandHandler {
499526
(source instanceof CommandInteraction ||
500527
source instanceof AutocompleteInteraction) &&
501528
source.guildId &&
502-
loadedCommand.guilds?.length &&
503-
!loadedCommand.guilds.includes(source.guildId)
529+
loadedCommand.metadata?.guilds?.length &&
530+
!loadedCommand.metadata?.guilds.includes(source.guildId)
504531
) {
505532
return null;
506533
}
@@ -516,6 +543,24 @@ export class AppCommandHandler {
516543
}
517544
}
518545

546+
if (!getConfig().disablePermissionsMiddleware) {
547+
middlewares.push({
548+
data: {
549+
// @ts-ignore
550+
beforeExecute,
551+
},
552+
middleware: {
553+
command: null,
554+
global: true,
555+
id: middlewareId,
556+
name: 'permissions',
557+
parentPath: '',
558+
path: '',
559+
relativePath: '',
560+
},
561+
});
562+
}
563+
519564
// No middleware for subcommands since they inherit from parent command
520565
return {
521566
command: loadedCommand,
@@ -536,7 +581,7 @@ export class AppCommandHandler {
536581
}
537582

538583
// Check aliases for prefix commands
539-
const aliases = loadedCommand.data.command.aliases;
584+
const aliases = loadedCommand.data.metadata?.aliases;
540585
if (aliases && Array.isArray(aliases) && aliases.includes(name)) {
541586
return loadedCommand;
542587
}
@@ -640,7 +685,7 @@ export class AppCommandHandler {
640685
(v) => v.data.command.name,
641686
);
642687
const aliases = Array.from(this.loadedCommands.values()).flatMap(
643-
(v) => v.data.command.aliases || [],
688+
(v) => v.metadata.aliases || [],
644689
);
645690

646691
const allNames = [...commandNames, ...aliases];
@@ -698,6 +743,12 @@ export class AppCommandHandler {
698743
if (command.path === null) {
699744
this.loadedCommands.set(id, {
700745
command,
746+
metadata: {
747+
guilds: [],
748+
aliases: [],
749+
userPermissions: [],
750+
botPermissions: [],
751+
},
701752
data: {
702753
command: {
703754
name: command.name,
@@ -719,6 +770,22 @@ export class AppCommandHandler {
719770
);
720771
}
721772

773+
const metadataFunc = commandFileData.generateMetadata;
774+
const metadataObj = commandFileData.metadata;
775+
776+
if (metadataFunc && metadataObj) {
777+
throw new Error(
778+
'A command may only export either `generateMetadata` or `metadata`, not both',
779+
);
780+
}
781+
782+
const metadata = (metadataFunc ? await metadataFunc() : metadataObj) ?? {
783+
aliases: [],
784+
guilds: [],
785+
userPermissions: [],
786+
botPermissions: [],
787+
};
788+
722789
// Apply the specified logic for name and description
723790
const commandName = commandFileData.command.name || command.name;
724791
const commandDescription =
@@ -729,7 +796,6 @@ export class AppCommandHandler {
729796
...commandFileData.command,
730797
name: commandName,
731798
description: commandDescription,
732-
aliases: commandFileData.command.aliases,
733799
} as CommandData;
734800

735801
let handlerCount = 0;
@@ -747,7 +813,7 @@ export class AppCommandHandler {
747813
);
748814
}
749815

750-
if (key !== 'command') {
816+
if (!KNOWN_NON_HANDLER_KEYS.includes(key)) {
751817
// command file includes a handler function (chatInput, message, etc)
752818
handlerCount++;
753819
}
@@ -770,19 +836,53 @@ export class AppCommandHandler {
770836
}
771837
});
772838

839+
const commandJson =
840+
'toJSON' in lastUpdated && typeof lastUpdated.toJSON === 'function'
841+
? lastUpdated.toJSON()
842+
: lastUpdated;
843+
844+
if ('guilds' in commandJson || 'aliases' in commandJson) {
845+
Logger.warn(
846+
`Command \`${command.name}\` uses deprecated metadata properties. Please update to use the new \`metadata\` object or \`generateMetadata\` function.`,
847+
);
848+
}
849+
773850
this.loadedCommands.set(id, {
774851
command,
775-
guilds: commandFileData.command.guilds,
852+
metadata: {
853+
guilds: commandJson.guilds,
854+
aliases: commandJson.aliases,
855+
...metadata,
856+
},
776857
data: {
777858
...commandFileData,
778-
command:
779-
'toJSON' in lastUpdated && typeof lastUpdated.toJSON === 'function'
780-
? lastUpdated.toJSON()
781-
: lastUpdated,
859+
metadata: {
860+
guilds: commandJson.guilds,
861+
aliases: commandJson.aliases,
862+
...metadata,
863+
},
864+
command: commandJson,
782865
},
783866
});
784867
} catch (error) {
785868
Logger.error(`Failed to load command ${command.name} (${id})`, error);
786869
}
787870
}
871+
872+
/**
873+
* Gets the metadata for a command.
874+
* @param command - The command name to get metadata for
875+
* @returns The command metadata or null if not found
876+
*/
877+
public getMetadataFor(command: string): CommandMetadata | null {
878+
const loadedCommand = this.findCommandByName(command);
879+
if (!loadedCommand) return null;
880+
881+
return (loadedCommand.metadata ??= {
882+
aliases: [],
883+
guilds: [],
884+
userPermissions: [],
885+
botPermissions: [],
886+
});
887+
}
788888
}

0 commit comments

Comments
 (0)