Skip to content

Commit cf3acfb

Browse files
committed
fix: user/bot permissions middleware to support message commands + acknowledge dms
1 parent 7f807c1 commit cf3acfb

File tree

5 files changed

+105
-49
lines changed

5 files changed

+105
-49
lines changed

apps/test-bot/src/app.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Client } from 'discord.js';
1+
import { Client, Partials } from 'discord.js';
22
import { Logger, commandkit } from 'commandkit';
33
import { setDriver } from '@commandkit/tasks';
44
import { SQLiteDriver } from '@commandkit/tasks/sqlite';
@@ -11,7 +11,9 @@ const client = new Client({
1111
'GuildMessages',
1212
'MessageContent',
1313
'GuildMessageTyping',
14+
'DirectMessages',
1415
],
16+
partials: [Partials.Channel, Partials.Message, Partials.User],
1517
});
1618

1719
setDriver(new SQLiteDriver('./tasks.db'));

apps/test-bot/src/app/commands/(general)/ping.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,16 @@
11
import {
22
ActionRowBuilder,
33
ApplicationCommandOptionType,
4+
ApplicationIntegrationType,
45
ButtonStyle,
56
} from 'discord.js';
67
import {
78
CommandData,
89
ButtonKit,
910
ChatInputCommandContext,
1011
AutocompleteCommandContext,
12+
CommandMetadata,
13+
MessageCommandContext,
1114
} from 'commandkit';
1215

1316
export const command: CommandData = {
@@ -22,6 +25,17 @@ export const command: CommandData = {
2225
required: false,
2326
},
2427
],
28+
integration_types: [
29+
ApplicationIntegrationType.GuildInstall,
30+
ApplicationIntegrationType.UserInstall,
31+
],
32+
// guilds: ['1314834483660455938'],
33+
};
34+
35+
export const metadata: CommandMetadata = {
36+
userPermissions: 'Administrator',
37+
botPermissions: 'KickMembers',
38+
// guilds: ['1314834483660455938'],
2539
};
2640

2741
const tests = Array.from({ length: 10 }, (_, i) => ({
@@ -43,6 +57,10 @@ export async function autocomplete({
4357
interaction.respond(filtered);
4458
}
4559

60+
export async function message({ message }: MessageCommandContext) {
61+
message.reply('Pong!');
62+
}
63+
4664
export async function chatInput({
4765
interaction,
4866
client,

apps/website/docs/guide/02-commands/07-middlewares.mdx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ export function afterExecute(ctx: MiddlewareContext) {
3636

3737
## Stop command execution
3838

39-
You can stop a command from running by calling `ctx.cancel()` in the
39+
You can stop a command from running by returning `ctx.cancel()` in the
4040
`beforeExecute` function.
4141

4242
```ts title="src/app/commands/+middleware.ts"
@@ -46,7 +46,7 @@ export function beforeExecute(ctx: MiddlewareContext) {
4646
if (ctx.interaction.user.id !== '1234567890') {
4747
// Conditionally stop command execution
4848
console.log(`${ctx.commandName} will not be executed!`);
49-
ctx.cancel();
49+
return ctx.cancel();
5050
}
5151

5252
// Continue with command execution
Lines changed: 77 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,7 @@
1-
import {
2-
EmbedBuilder,
3-
PermissionFlags,
4-
PermissionFlagsBits,
5-
PermissionResolvable,
6-
} from 'discord.js';
7-
import { MiddlewareContext } from '../commands/Context';
1+
import { EmbedBuilder, MessageFlags } from 'discord.js';
82
import { getConfig } from '../../config/config';
9-
10-
const findName = (flags: PermissionFlags, flag: PermissionResolvable) => {
11-
if (typeof flag === 'string') return flag;
12-
13-
return (
14-
Object.entries(flags).find(([_, value]) => value === flag)?.[0] ?? `${flag}`
15-
);
16-
};
3+
import { Logger } from '../../logger/Logger';
4+
import { MiddlewareContext } from '../commands/Context';
175

186
export const middlewareId = crypto.randomUUID();
197

@@ -22,55 +10,85 @@ export const middlewareId = crypto.randomUUID();
2210
* @ignore
2311
*/
2412
export async function beforeExecute(ctx: MiddlewareContext) {
25-
if (getConfig().disablePermissionsMiddleware) return;
13+
if (getConfig().disablePermissionsMiddleware) {
14+
return;
15+
}
16+
17+
const { interaction, message, command } = ctx;
18+
19+
if (interaction && !interaction.isCommand()) {
20+
return;
21+
}
2622

27-
const { interaction, command } = ctx;
23+
if (
24+
(interaction && interaction.channel?.isDMBased()) ||
25+
(message && message.channel?.isDMBased())
26+
) {
27+
const channel = interaction?.channel ?? message?.channel;
28+
29+
const embed = new EmbedBuilder()
30+
.setTitle(':x: Server-only command!')
31+
.setDescription('This command can only be used in a server.')
32+
.setColor('Red');
33+
34+
try {
35+
if (channel?.isSendable()) {
36+
if (interaction && interaction.isRepliable()) {
37+
await interaction.reply({
38+
embeds: [embed],
39+
flags: MessageFlags.Ephemeral,
40+
});
41+
} else {
42+
await message.reply({ embeds: [embed] });
43+
}
44+
}
45+
} catch (error) {
46+
Logger.error(
47+
`Could not send 'Server-only command' DM to user ${interaction?.user.id ?? message?.author.id} for command ${command.command.name}.`,
48+
error,
49+
);
50+
}
51+
52+
return ctx.cancel(); // Stop the command from executing
53+
}
2854

29-
if (interaction.isAutocomplete()) return;
30-
const userPermissions = interaction.memberPermissions;
31-
let userPermissionsRequired = command.metadata?.userPermissions;
55+
const userPermissions =
56+
interaction?.memberPermissions ?? message?.member?.permissions;
57+
let userPermissionsRequired = command.metadata?.userPermissions ?? [];
3258
let missingUserPermissions: string[] = [];
3359

3460
if (typeof userPermissionsRequired === 'string') {
3561
userPermissionsRequired = [userPermissionsRequired];
3662
}
3763

38-
const botPermissions = interaction.guild?.members.me?.permissions;
39-
let botPermissionsRequired = command.metadata?.botPermissions;
64+
const botPermissions =
65+
interaction?.guild?.members.me?.permissions ??
66+
message?.guild?.members.me?.permissions;
67+
let botPermissionsRequired = command.metadata?.botPermissions ?? [];
4068
let missingBotPermissions: string[] = [];
4169

4270
if (typeof botPermissionsRequired === 'string') {
4371
botPermissionsRequired = [botPermissionsRequired];
4472
}
4573

46-
if (!userPermissionsRequired?.length && !botPermissionsRequired?.length) {
74+
if (!userPermissionsRequired.length && !botPermissionsRequired.length) {
4775
return;
4876
}
4977

50-
if (userPermissions && userPermissionsRequired) {
78+
if (userPermissionsRequired.length) {
5179
for (const permission of userPermissionsRequired) {
52-
const hasPermission = userPermissions.has(permission);
53-
80+
const hasPermission = userPermissions?.has(permission);
5481
if (!hasPermission) {
55-
missingUserPermissions.push(
56-
typeof permission === 'string'
57-
? permission
58-
: findName(PermissionFlagsBits, permission),
59-
);
82+
missingUserPermissions.push(permission);
6083
}
6184
}
6285
}
6386

64-
if (botPermissions && botPermissionsRequired) {
87+
if (botPermissionsRequired.length) {
6588
for (const permission of botPermissionsRequired) {
66-
const hasPermission = botPermissions.has(permission);
67-
89+
const hasPermission = botPermissions?.has(permission);
6890
if (!hasPermission) {
69-
missingBotPermissions.push(
70-
typeof permission === 'string'
71-
? permission
72-
: findName(PermissionFlagsBits, permission),
73-
);
91+
missingBotPermissions.push(permission);
7492
}
7593
}
7694
}
@@ -79,7 +97,7 @@ export async function beforeExecute(ctx: MiddlewareContext) {
7997
return;
8098
}
8199

82-
// Fix casing. e.g. KickMembers -> Kick Members
100+
// Fix permission string. e.g. KickMembers -> Kick Members
83101
const pattern = /([a-z])([A-Z])|([A-Z]+)([A-Z][a-z])/g;
84102

85103
missingUserPermissions = missingUserPermissions.map((str) =>
@@ -123,6 +141,23 @@ export async function beforeExecute(ctx: MiddlewareContext) {
123141
.setDescription(embedDescription)
124142
.setColor('Red');
125143

126-
await interaction.reply({ embeds: [embed], ephemeral: true });
127-
return true;
144+
try {
145+
if (interaction && interaction.isRepliable()) {
146+
await interaction.reply({
147+
embeds: [embed],
148+
flags: MessageFlags.Ephemeral,
149+
});
150+
} else if (message && message.channel?.isSendable()) {
151+
await message.reply({
152+
embeds: [embed],
153+
});
154+
}
155+
} catch (error) {
156+
Logger.error(
157+
`Could not send 'Not enough permissions' reply to user ${interaction?.user.id ?? message?.author.id} for command ${command.command.name}.`,
158+
error,
159+
);
160+
}
161+
162+
return ctx.cancel(); // Stop the command from executing
128163
}

packages/commandkit/src/types.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type {
44
ClientEvents,
55
Interaction,
66
PermissionResolvable,
7+
PermissionsString,
78
RESTPostAPIApplicationCommandsJSONBody,
89
} from 'discord.js';
910
import type { CommandKit } from './commandkit';
@@ -58,11 +59,11 @@ export interface CommandMetadata {
5859
/**
5960
* The user permissions required to execute the command.
6061
*/
61-
userPermissions?: PermissionResolvable[];
62+
userPermissions?: PermissionsString | PermissionsString[];
6263
/**
6364
* The bot permissions required to execute the command.
6465
*/
65-
botPermissions?: PermissionResolvable[];
66+
botPermissions?: PermissionsString | PermissionsString[];
6667
}
6768

6869
/**
@@ -71,12 +72,12 @@ export interface CommandMetadata {
7172
export interface LegacyCommandMetadata {
7273
/**
7374
* The aliases of the command.
74-
* @deprecated Use `metadata` or `generateMetadata` instead.
75+
* @deprecated Use `metadata.aliases` or `generateMetadata` instead.
7576
*/
7677
aliases?: string[];
7778
/**
7879
* The guilds that the command is available in.
79-
* @deprecated Use `metadata` or `generateMetadata` instead.
80+
* @deprecated Use `metadata.guilds` or `generateMetadata` instead.
8081
*/
8182
guilds?: string[];
8283
}

0 commit comments

Comments
 (0)