Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

v2.0.0 #14

Merged
merged 3 commits into from
Sep 10, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/assets/search.js

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/index.html

Large diffs are not rendered by default.

20 changes: 7 additions & 13 deletions docs/interfaces/MarshalOptions.html

Large diffs are not rendered by default.

65 changes: 22 additions & 43 deletions docs/modules.html

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "djs-marshal",
"description": "A simple slash command manager for typescript",
"version": "1.3.2",
"version": "2.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"repository": "https://github.com/DeathVenom54/djs-marshal.git",
Expand All @@ -22,14 +22,14 @@
"docs": "typedoc --plugin ./node_modules/typedoc-theme-hierarchy/dist/index.js"
},
"peerDependencies": {
"discord.js": ">=13.x.x"
"discord.js": ">=14.x.x"
},
"devDependencies": {
"@types/chalk": "^2.2.0",
"@types/deep-equal": "^1.0.1",
"@typescript-eslint/eslint-plugin": "^5.21.0",
"@typescript-eslint/parser": "^5.21.0",
"discord.js": "^13.6.0",
"discord.js": "14",
"eslint": "^8.14.0",
"eslint-config-prettier": "^8.5.0",
"eslint-plugin-prettier": "^4.0.0",
Expand Down
11 changes: 6 additions & 5 deletions src/core/buttons/loadButtons.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { ButtonCommand } from '../../structures/ButtonCommand';
import { Client, Collection } from 'discord.js';
import { logVerbose, logWarning } from '../../utils/logger';

/**
* Loads the buttons provided into client.buttons
Expand All @@ -9,21 +8,23 @@ import { logVerbose, logWarning } from '../../utils/logger';
* @param {ButtonCommand[]} buttons The buttons to load
*/
export const loadButtons = (client: Client, buttons: ButtonCommand[]): void => {
logVerbose('Loading buttons', client);
client.logMethod('Loading buttons', 'verbose');
const buttonCollection = new Collection<string | RegExp, ButtonCommand>();

buttons.forEach((button) => {
// checks
if (button.beforeExecute?.deferReplyEphemeral && button.beforeExecute?.deferReply) {
logWarning(`deferReply and deferReplyEphemeral are both set to true for button ${button.customId}`, client);
client.logMethod(
`deferReply and deferReplyEphemeral are both set to true for button ${button.customId}`,
'erroronly',
);
}
const defer = button.beforeExecute?.deferReplyEphemeral || button.beforeExecute?.deferReply;
if (defer && button.beforeExecute?.deferUpdate) {
logWarning(`deferReply and deferUpdate are both set to true for button ${button.customId}`, client);
client.logMethod(`deferReply and deferUpdate are both set to true for button ${button.customId}`, 'erroronly');
}

buttonCollection.set(button.customId, button);
logVerbose(` ✅ ${button.customId}`, client);
});

client.buttons = buttonCollection;
Expand Down
19 changes: 6 additions & 13 deletions src/core/commands/loadCommands.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import { SlashCommand } from '../../structures/SlashCommand';
import { Client, Collection, Snowflake } from 'discord.js';
import { Client, Collection, Snowflake, ApplicationCommandType } from 'discord.js';
import { syncCommands } from './syncCommands';
import { logWarning, logVerbose, logError } from '../../utils/logger';

/**
* Loads slash commands and stores them as client.commands, then syncs them with Discord
Expand All @@ -15,18 +14,14 @@ export const loadCommands = async (client: Client, commands: SlashCommand[]): Pr
commandsCollection.set('global', []);
commandsCollection.set('allGuild', []);

logVerbose('Loading commands', client);
client.logMethod('Loading commands', 'verbose');
commands.forEach((command) => {
// preload checks
if (command.beforeExecute?.defer && command.beforeExecute?.deferEphemeral)
logWarning(`defer and deferEphemeral are both true for command ${command.name}`, client);
if ('allowWithPermission' in command && command.allowWithPermission === [])
logWarning(`allowWithPermission is [] for ${command.name}, it will be ignored`, client);
client.logMethod(`defer and deferEphemeral are both true for command ${command.name}`, 'erroronly');

if (!command.type) command.type = 'CHAT_INPUT';
if (!command.defaultPermission) command.defaultPermission = true;
if (!command.type) command.type = ApplicationCommandType.ChatInput;
if (command.handleError === undefined) command.handleError = true;
if ('allowWithPermission' in command) command.defaultPermission = !command.allowWithPermission?.length;

// is guild command
if (command.commandType === 'guild' && command.guildId) {
Expand All @@ -38,11 +33,9 @@ export const loadCommands = async (client: Client, commands: SlashCommand[]): Pr
} else {
commandsCollection.get('global')?.push(command);
}

logVerbose(` ✅ ${command.name}`, client);
});

client.commands = commandsCollection;
logVerbose('Loaded all commands', client);
await syncCommands(client).catch((err) => logError(err, client));
client.logMethod('Loaded all commands', 'verbose');
await syncCommands(client).catch((err) => client.logMethod(err, 'erroronly'));
};
171 changes: 8 additions & 163 deletions src/core/commands/syncCommands.ts
Original file line number Diff line number Diff line change
@@ -1,55 +1,16 @@
import {
ApplicationCommand,
ApplicationCommandPermissionData,
Client,
ClientApplication,
Guild,
Intents,
Role,
Snowflake,
} from 'discord.js';
import { Client, ClientApplication, Guild } from 'discord.js';
import { SlashCommand } from '../../structures/SlashCommand';
import deepEqual from 'deep-equal';
import { logVerbose } from '../../utils/logger';
import { toApplicationCommand } from '../../utils/toApplicationCommand';

/**
* Sets the permissions of a registered ApplicationCommand according to SlashCommand properties
*
* @param {ApplicationCommand} registered The registered application command
* @param {SlashCommand} command The registered slash command
*/
const setPermissions = async (registered: ApplicationCommand, command: SlashCommand): Promise<void> => {
const permissions: ApplicationCommandPermissionData[] = (command.allowRoles || []).map((id) => {
return { id, type: 'ROLE', permission: true };
});
permissions.push(
...(command.denyRoles || []).map((id) => {
return <ApplicationCommandPermissionData>{ id, type: 'ROLE', permission: false };
}),
);
permissions.push(
...(command.allowUsers || []).map((id) => {
return <ApplicationCommandPermissionData>{ id, type: 'USER', permission: true };
}),
);
permissions.push(
...(command.denyUsers || []).map((id) => {
return <ApplicationCommandPermissionData>{ id, type: 'USER', permission: false };
}),
);

if (permissions.length) await registered.permissions.set({ permissions });
};

/**
* Compares and syncs global commands if needed
*
* @param application The client's application (client.application)
* @param {SlashCommand[]} newCommands The commands to sync
*/
const syncGlobalCommands = async (application: ClientApplication, newCommands: SlashCommand[]): Promise<void> => {
logVerbose('Syncing global commands', application.client);
application.client.logMethod('Syncing global commands', 'verbose');
const currentCommands = await application.commands.fetch();
const cc = [...currentCommands.values()];

Expand All @@ -59,21 +20,20 @@ const syncGlobalCommands = async (application: ClientApplication, newCommands: S

// command is new
if (!matching) {
logVerbose(`Syncing new global command: ${command.name}`, application.client);
application.client.logMethod(`Syncing new global command: ${command.name}`, 'verbose');
await application.commands.create(command);
logVerbose(` ✅ ${command.name}`, application.client);
continue;
}

// command has changed
if (!deepEqual(matching, toApplicationCommand(command))) {
logVerbose(`Syncing changed global command: ${command.name}`, application.client);
application.client.logMethod(`Syncing changed global command: ${command.name}`, 'verbose');
await application.commands.edit(matching.id, command);
}

// finally, remove from synced commands
cc.splice(cc.indexOf(matching), 1);
logVerbose(` ✅ ${command.name}`, application.client);
application.client.logMethod(` ✅ ${command.name}`, 'verbose');
}

// delete left over commands
Expand All @@ -86,93 +46,6 @@ const syncGlobalCommands = async (application: ClientApplication, newCommands: S
}
};

const syncPermissionForRole = async (
command: SlashCommand,
role: Role,
): Promise<{
allowedRoles: Snowflake[];
allowedMembers: Snowflake[];
}> => {
const allowedRoles: Snowflake[] = [];
const allowedMembers: Snowflake[] = [];
const seenMembers: Snowflake[] = [];

const isAllowed = command.allowWithPermission?.some((perm) => role.permissions.has(perm));
if (isAllowed) allowedRoles.push(role.id);
// doesn't have permission, check members individually
else {
role.members.forEach((member) => {
// make sure member is not repeated
if (seenMembers.includes(member.id)) return;

const memberAllowed = command.allowWithPermission?.some((perm) => member.permissions.has(perm));
if (memberAllowed) allowedMembers.push(member.id);

seenMembers.push(member.id);
});
}

return { allowedRoles, allowedMembers };
};

/**
* Makes sure users with only selected permissions can use the command
*
* @param guild The guild in which command should be checked
* @param command The SlashCommand to be checked
* @param existing The existing ApplicationCommand in guild
*
* @deprecated Buggy and resource heavy, use allowRoles, denyRoles, allowUsers and denyUsers
*/
export const syncPermissions = async (
guild: Guild,
command: SlashCommand,
existing: ApplicationCommand,
): Promise<void> => {
if ('allowWithPermission' in command && command.allowWithPermission?.length) {
// this won't work without GUILDS and GUILD_MEMBERS intents
if (!new Intents(guild.client.options.intents).has('GUILD_MEMBERS'))
throw new Error('allowWithPermission requires the GUILD_MEMBERS intent');

// fetch members for cache
await guild.members.fetch();

const roles = await guild.roles.fetch();
let everyoneRole: Role | undefined = undefined;

const aRoles: Snowflake[] = [];
const aMembers: Snowflake[] = [];

for (const [, role] of roles) {
// ensure that @everyone is checked last
if (role.name === '@everyone') {
everyoneRole = role;
continue;
}

const { allowedMembers, allowedRoles } = await syncPermissionForRole(command, role);
aMembers.push(...allowedMembers);
aRoles.push(...allowedRoles);
}
if (everyoneRole) await syncPermissionForRole(command, everyoneRole);

// add permissions
if (aMembers.length || aRoles.length) {
const commandPerms = aMembers
.map((id) => {
return { type: 'USER', id, permission: true };
})
.concat(
aRoles.map((id) => {
return { type: 'ROLE', id, permission: true };
}),
) as ApplicationCommandPermissionData[];

await existing.permissions.set({ permissions: commandPerms });
}
}
};

/**
* Sync commands of a guild
*
Expand All @@ -182,35 +55,7 @@ export const syncGuildCommands = async (guild: Guild): Promise<void> => {
const commands = guild.client.commands;
const guildCommands = (commands.get('allGuild') || [])?.concat(commands.get(guild.id) || []);

await guild.commands.set([]);

const withoutPerms: SlashCommand[] = [];
const withPerms: SlashCommand[] = [];

guildCommands.forEach((c) => {
if (c.allowRoles?.length || c.denyRoles?.length || c.allowUsers?.length || c.denyUsers?.length) {
withPerms.push(c);
} else {
withoutPerms.push(c);
}
});
await guild.commands.set(withoutPerms);
for (const command of withPerms) {
const registered = await guild.commands.create(command);
await setPermissions(registered, command);
}

// sync permission
await Promise.all(
guildCommands.map(async (com) => {
if ('allowWithPermission' in com && com.allowWithPermission?.length) {
const registered = guild.commands.cache.find((c) => c.name === com.name);

if (!registered) return;
await syncPermissions(guild, com, registered);
}
}),
);
await guild.commands.set(guildCommands);
};

/**
Expand All @@ -230,12 +75,12 @@ export const syncCommands = async (client: Client): Promise<void> => {
if (global) await syncGlobalCommands(application, global);

// sync guild commands
logVerbose(`Syncing guild commands`, client);
application.client.logMethod(`Syncing guild commands`, 'verbose');
const guilds = await client.guilds.fetch();
for (const [, g] of guilds) {
const guild = await g.fetch();
await syncGuildCommands(guild);
}

logVerbose('Successfully synced commands', client);
application.client.logMethod('Successfully synced commands', 'verbose');
};
5 changes: 2 additions & 3 deletions src/core/handlers/handleButtonInteraction.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { ButtonInteraction } from 'discord.js';
import { logWarning } from '../../utils/logger';

/**
* Intercepts a ButtonInteraction and executes the matching button
Expand All @@ -12,9 +11,9 @@ export const handleButtonInteraction = async (int: ButtonInteraction): Promise<v
});

if (!button)
return logWarning(
return int.client.logMethod(
`Received Button interaction with customId: ${int.customId}, but no such registered button`,
int.client,
'warn',
);

// beforeExecute stuff
Expand Down
36 changes: 0 additions & 36 deletions src/core/handlers/handleGuildMemberUpdate.ts

This file was deleted.

Loading