Reworked permission handling

This commit is contained in:
WatDuhHekBro 2021-03-30 05:54:52 -05:00
parent 51fa9457b4
commit 475ecb3d5d
7 changed files with 95 additions and 86 deletions

View file

@ -1,7 +1,7 @@
import Command, {handler} from "../core/command"; import Command, {handler} from "../core/command";
import {botHasPermission, clean} from "../core/libd"; import {botHasPermission, clean} from "../core/libd";
import {Config, Storage} from "../core/structures"; import {Config, Storage} from "../core/structures";
import {PermissionNames, getPermissionLevel} from "../core/permissions"; import {getPermissionLevel, getPermissionName} from "../core/permissions";
import {Permissions} from "discord.js"; import {Permissions} from "discord.js";
import * as discord from "discord.js"; import * as discord from "discord.js";
import {logs} from "../globals"; import {logs} from "../globals";
@ -30,14 +30,14 @@ export default new Command({
} }
const permLevel = getPermissionLevel($.member); const permLevel = getPermissionLevel($.member);
$.channel.send( $.channel.send(
`${$.author.toString()}, your permission level is \`${PermissionNames[permLevel]}\` (${permLevel}).` `${$.author.toString()}, your permission level is \`${getPermissionName(permLevel)}\` (${permLevel}).`
); );
}, },
subcommands: { subcommands: {
set: new Command({ set: new Command({
description: "Set different per-guild settings for the bot.", description: "Set different per-guild settings for the bot.",
run: "You have to specify the option you want to set.", run: "You have to specify the option you want to set.",
permission: Command.PERMISSIONS.ADMIN, permission: PERMISSIONS.ADMIN,
subcommands: { subcommands: {
prefix: new Command({ prefix: new Command({
description: "Set a custom prefix for your guild. Removes your custom prefix if none is provided.", description: "Set a custom prefix for your guild. Removes your custom prefix if none is provided.",
@ -61,7 +61,7 @@ export default new Command({
}), }),
diag: new Command({ diag: new Command({
description: 'Requests a debug log with the "info" verbosity level.', description: 'Requests a debug log with the "info" verbosity level.',
permission: Command.PERMISSIONS.BOT_SUPPORT, permission: PERMISSIONS.BOT_SUPPORT,
async run($) { async run($) {
$.channel.send(getLogBuffer("info")); $.channel.send(getLogBuffer("info"));
}, },
@ -82,7 +82,7 @@ export default new Command({
}), }),
status: new Command({ status: new Command({
description: "Changes the bot's status.", description: "Changes the bot's status.",
permission: Command.PERMISSIONS.BOT_SUPPORT, permission: PERMISSIONS.BOT_SUPPORT,
async run($) { async run($) {
$.channel.send("Setting status to `online`..."); $.channel.send("Setting status to `online`...");
}, },
@ -101,7 +101,7 @@ export default new Command({
}), }),
purge: new Command({ purge: new Command({
description: "Purges bot messages.", description: "Purges bot messages.",
permission: Command.PERMISSIONS.BOT_SUPPORT, permission: PERMISSIONS.BOT_SUPPORT,
async run($) { async run($) {
if ($.message.channel instanceof discord.DMChannel) { if ($.message.channel instanceof discord.DMChannel) {
return; return;
@ -142,7 +142,7 @@ export default new Command({
eval: new Command({ eval: new Command({
description: "Evaluate code.", description: "Evaluate code.",
usage: "<code>", usage: "<code>",
permission: Command.PERMISSIONS.BOT_OWNER, permission: PERMISSIONS.BOT_OWNER,
// You have to bring everything into scope to use them. AFAIK, there isn't a more maintainable way to do this, but at least TS will let you know if anything gets removed. // You have to bring everything into scope to use them. AFAIK, there isn't a more maintainable way to do this, but at least TS will let you know if anything gets removed.
async run({args, author, channel, client, guild, member, message}) { async run({args, author, channel, client, guild, member, message}) {
try { try {
@ -158,7 +158,7 @@ export default new Command({
}), }),
nick: new Command({ nick: new Command({
description: "Change the bot's nickname.", description: "Change the bot's nickname.",
permission: Command.PERMISSIONS.BOT_SUPPORT, permission: PERMISSIONS.BOT_SUPPORT,
async run($) { async run($) {
const nickName = $.args.join(" "); const nickName = $.args.join(" ");
await $.guild?.me?.setNickname(nickName); await $.guild?.me?.setNickname(nickName);
@ -169,7 +169,7 @@ export default new Command({
}), }),
guilds: new Command({ guilds: new Command({
description: "Shows a list of all guilds the bot is a member of.", description: "Shows a list of all guilds the bot is a member of.",
permission: Command.PERMISSIONS.BOT_SUPPORT, permission: PERMISSIONS.BOT_SUPPORT,
async run($) { async run($) {
const guildList = $.client.guilds.cache.array().map((e) => e.name); const guildList = $.client.guilds.cache.array().map((e) => e.name);
$.channel.send(guildList); $.channel.send(guildList);
@ -177,7 +177,7 @@ export default new Command({
}), }),
activity: new Command({ activity: new Command({
description: "Set the activity of the bot.", description: "Set the activity of the bot.",
permission: Command.PERMISSIONS.BOT_SUPPORT, permission: PERMISSIONS.BOT_SUPPORT,
usage: "<type> <string>", usage: "<type> <string>",
async run($) { async run($) {
$.client.user?.setActivity(".help", { $.client.user?.setActivity(".help", {

View file

@ -1,7 +1,7 @@
import Command from "../core/command"; import Command from "../core/command";
import {toTitleCase} from "../core/lib"; import {toTitleCase} from "../core/lib";
import {loadableCommands, categories} from "../core/command"; import {loadableCommands, categories} from "../core/command";
import {PermissionNames} from "../core/permissions"; import {getPermissionName} from "../core/permissions";
export default new Command({ export default new Command({
description: "Lists all commands. If a command is specified, their arguments are listed as well.", description: "Lists all commands. If a command is specified, their arguments are listed as well.",
@ -44,7 +44,7 @@ export default new Command({
if (command.originalCommandName) header = command.originalCommandName; if (command.originalCommandName) header = command.originalCommandName;
else console.warn(`originalCommandName isn't defined for ${header}?!`); else console.warn(`originalCommandName isn't defined for ${header}?!`);
let permLevel = command.permission ?? Command.PERMISSIONS.NONE; let permLevel = command.permission ?? 0;
let usage = command.usage; let usage = command.usage;
let invalid = false; let invalid = false;
@ -135,7 +135,9 @@ export default new Command({
} }
$.channel.send( $.channel.send(
`Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${selectedCategory}\`\nPermission Required: \`${PermissionNames[permLevel]}\` (${permLevel})\nDescription: ${command.description}\n${append}`, `Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${selectedCategory}\`\nPermission Required: \`${getPermissionName(
permLevel
)}\` (${permLevel})\nDescription: ${command.description}\n${append}`,
{split: true} {split: true}
); );
} }

View file

@ -5,7 +5,7 @@ export default new Command({
'This is a template/testing command providing common functionality. Remove what you don\'t need, and rename/delete this file to generate a fresh command file here. This command should be automatically excluded from the help command. The "usage" parameter (string) overrides the default usage for the help command. The "endpoint" parameter (boolean) prevents further arguments from being passed. Also, as long as you keep the run function async, it\'ll return a promise allowing the program to automatically catch any synchronous errors. However, you\'ll have to do manual error handling if you go the then and catch route.', 'This is a template/testing command providing common functionality. Remove what you don\'t need, and rename/delete this file to generate a fresh command file here. This command should be automatically excluded from the help command. The "usage" parameter (string) overrides the default usage for the help command. The "endpoint" parameter (boolean) prevents further arguments from being passed. Also, as long as you keep the run function async, it\'ll return a promise allowing the program to automatically catch any synchronous errors. However, you\'ll have to do manual error handling if you go the then and catch route.',
endpoint: false, endpoint: false,
usage: "", usage: "",
permission: null, permission: -1,
aliases: [], aliases: [],
async run($) { async run($) {
// code // code
@ -16,7 +16,7 @@ export default new Command({
'This is a named subcommand, meaning that the key name is what determines the keyword to use. With default settings for example, "$test layer".', 'This is a named subcommand, meaning that the key name is what determines the keyword to use. With default settings for example, "$test layer".',
endpoint: false, endpoint: false,
usage: "", usage: "",
permission: null, permission: -1,
aliases: [], aliases: [],
async run($) { async run($) {
// code // code
@ -28,7 +28,7 @@ export default new Command({
'This is the subcommand for getting users by pinging them or copying their ID. With default settings for example, "$test 237359961842253835". The argument will be a user object and won\'t run if no user is found by that ID.', 'This is the subcommand for getting users by pinging them or copying their ID. With default settings for example, "$test 237359961842253835". The argument will be a user object and won\'t run if no user is found by that ID.',
endpoint: false, endpoint: false,
usage: "", usage: "",
permission: null, permission: -1,
async run($) { async run($) {
// code // code
} }
@ -38,7 +38,7 @@ export default new Command({
'This is a numeric subcommand, meaning that any type of number (excluding Infinity/NaN) will route to this command if present. With default settings for example, "$test -5.2". The argument with the number is already parsed so you can just use it without converting it.', 'This is a numeric subcommand, meaning that any type of number (excluding Infinity/NaN) will route to this command if present. With default settings for example, "$test -5.2". The argument with the number is already parsed so you can just use it without converting it.',
endpoint: false, endpoint: false,
usage: "", usage: "",
permission: null, permission: -1,
async run($) { async run($) {
// code // code
} }
@ -48,7 +48,7 @@ export default new Command({
"This is a generic subcommand, meaning that if there isn't a more specific subcommand that's called, it falls to this. With default settings for example, \"$test reeee\".", "This is a generic subcommand, meaning that if there isn't a more specific subcommand that's called, it falls to this. With default settings for example, \"$test reeee\".",
endpoint: false, endpoint: false,
usage: "", usage: "",
permission: null, permission: -1,
async run($) { async run($) {
// code // code
} }

View file

@ -1,7 +1,6 @@
import {parseVars} from "./libd"; import {parseVars} from "./libd";
import {Collection} from "discord.js"; import {Collection} from "discord.js";
import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember} from "discord.js"; import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember} from "discord.js";
import {PERMISSIONS} from "./permissions";
import {getPrefix} from "../core/structures"; import {getPrefix} from "../core/structures";
import glob from "glob"; import glob from "glob";
@ -19,7 +18,7 @@ interface CommandOptions {
description?: string; description?: string;
endpoint?: boolean; endpoint?: boolean;
usage?: string; usage?: string;
permission?: PERMISSIONS | null; permission?: number;
aliases?: string[]; aliases?: string[];
run?: (($: CommandMenu) => Promise<any>) | string; run?: (($: CommandMenu) => Promise<any>) | string;
subcommands?: {[key: string]: Command}; subcommands?: {[key: string]: Command};
@ -40,7 +39,7 @@ export default class Command {
public readonly description: string; public readonly description: string;
public readonly endpoint: boolean; public readonly endpoint: boolean;
public readonly usage: string; public readonly usage: string;
public readonly permission: PERMISSIONS | null; public readonly permission: number; // -1 (default) indicates to inherit, 0 is the lowest rank, 1 is second lowest rank, and so on.
public readonly aliases: string[]; // This is to keep the array intact for parent Command instances to use. It'll also be used when loading top-level aliases. public readonly aliases: string[]; // This is to keep the array intact for parent Command instances to use. It'll also be used when loading top-level aliases.
public originalCommandName: string | null; // If the command is an alias, what's the original name? public originalCommandName: string | null; // If the command is an alias, what's the original name?
public run: (($: CommandMenu) => Promise<any>) | string; public run: (($: CommandMenu) => Promise<any>) | string;
@ -49,13 +48,12 @@ export default class Command {
public number: Command | null; public number: Command | null;
public any: Command | null; public any: Command | null;
public static readonly TYPES = TYPES; public static readonly TYPES = TYPES;
public static readonly PERMISSIONS = PERMISSIONS;
constructor(options?: CommandOptions) { constructor(options?: CommandOptions) {
this.description = options?.description || "No description."; this.description = options?.description || "No description.";
this.endpoint = options?.endpoint || false; this.endpoint = options?.endpoint || false;
this.usage = options?.usage || ""; this.usage = options?.usage || "";
this.permission = options?.permission ?? null; this.permission = options?.permission ?? -1;
this.aliases = options?.aliases ?? []; this.aliases = options?.aliases ?? [];
this.originalCommandName = null; this.originalCommandName = null;
this.run = options?.run || "No action was set on this command!"; this.run = options?.run || "No action was set on this command!";

View file

@ -1,75 +1,68 @@
import {GuildMember, Permissions} from "discord.js"; import {User, GuildMember, Permissions} from "discord.js";
import {Config} from "./structures"; import {Config} from "./structures";
export enum PERMISSIONS { interface PermissionLevel {
NONE, name: string;
MOD, check: (user: User, member: GuildMember | null) => boolean;
ADMIN,
OWNER,
BOT_SUPPORT,
BOT_ADMIN,
BOT_OWNER
} }
export const PermissionNames = [ export const PermissionLevels: PermissionLevel[] = [
"User", {
"Moderator", // NONE //
"Administrator", name: "User",
"Server Owner", check: () => true
"Bot Support", },
"Bot Admin", {
"Bot Owner" // MOD //
]; name: "Moderator",
check: (_, member) =>
// Here is where you enter in the functions that check for permissions. !!member &&
const PermissionChecker: ((member: GuildMember) => boolean)[] = [ (member.hasPermission(Permissions.FLAGS.MANAGE_ROLES) ||
// NONE // member.hasPermission(Permissions.FLAGS.MANAGE_MESSAGES) ||
() => true, member.hasPermission(Permissions.FLAGS.KICK_MEMBERS) ||
member.hasPermission(Permissions.FLAGS.BAN_MEMBERS))
// MOD // },
(member) => {
member.hasPermission(Permissions.FLAGS.MANAGE_ROLES) || // ADMIN //
member.hasPermission(Permissions.FLAGS.MANAGE_MESSAGES) || name: "Administrator",
member.hasPermission(Permissions.FLAGS.KICK_MEMBERS) || check: (_, member) => !!member && member.hasPermission(Permissions.FLAGS.ADMINISTRATOR)
member.hasPermission(Permissions.FLAGS.BAN_MEMBERS), },
{
// ADMIN // // OWNER //
(member) => member.hasPermission(Permissions.FLAGS.ADMINISTRATOR), name: "Server Owner",
check: (_, member) => !!member && member.guild.ownerID === member.id
// OWNER // },
(member) => member.guild.ownerID === member.id, {
// BOT_SUPPORT //
// BOT_SUPPORT // name: "Bot Support",
(member) => Config.support.includes(member.id), check: (user) => Config.support.includes(user.id)
},
// BOT_ADMIN // {
(member) => Config.admins.includes(member.id), // BOT_ADMIN //
name: "Bot Admin",
// BOT_OWNER // check: (user) => Config.admins.includes(user.id)
(member) => Config.owner === member.id },
{
// BOT_OWNER //
name: "Bot Owner",
check: (user) => Config.owner === user.id
}
]; ];
// After checking the lengths of these three objects, use this as the length for consistency. // After checking the lengths of these three objects, use this as the length for consistency.
const length = Object.keys(PERMISSIONS).length / 2; const length = PermissionLevels.length;
export function hasPermission(member: GuildMember, permission: PERMISSIONS): boolean { export function hasPermission(member: GuildMember, permission: number): boolean {
for (let i = length - 1; i >= permission; i--) if (PermissionChecker[i](member)) return true; for (let i = length - 1; i >= permission; i--) if (PermissionLevels[i].check(member.user, member)) return true;
return false; return false;
} }
export function getPermissionLevel(member: GuildMember): number { export function getPermissionLevel(member: GuildMember): number {
for (let i = length - 1; i >= 0; i--) if (PermissionChecker[i](member)) return i; for (let i = length - 1; i >= 0; i--) if (PermissionLevels[i].check(member.user, member)) return i;
return 0; return 0;
} }
// Length Checking export function getPermissionName(level: number) {
(() => { if (level > length || length < 0) return "N/A";
const lenNames = PermissionNames.length; else return PermissionLevels[level].name;
const lenChecker = PermissionChecker.length; }
// By transitive property, lenNames and lenChecker have to be equal to each other as well.
if (length !== lenNames || length !== lenChecker)
console.error(
`Permission object lengths aren't equal! Enum Length (${length}), Names Length (${lenNames}), and Functions Length (${lenChecker}). This WILL cause problems!`
);
})();

View file

@ -1,6 +1,6 @@
import Event from "../core/event"; import Event from "../core/event";
import Command, {loadableCommands} from "../core/command"; import Command, {loadableCommands} from "../core/command";
import {hasPermission, getPermissionLevel, PermissionNames} from "../core/permissions"; import {hasPermission, getPermissionLevel, getPermissionName} from "../core/permissions";
import {Permissions} from "discord.js"; import {Permissions} from "discord.js";
import {getPrefix} from "../core/structures"; import {getPrefix} from "../core/structures";
import {replyEventListeners} from "../core/libd"; import {replyEventListeners} from "../core/libd";
@ -91,7 +91,7 @@ export default new Event<"message">({
if (!command) return console.warn(`Command "${header}" was called but for some reason it's still undefined!`); if (!command) return console.warn(`Command "${header}" was called but for some reason it's still undefined!`);
const params: any[] = []; const params: any[] = [];
let isEndpoint = false; let isEndpoint = false;
let permLevel = command.permission ?? Command.PERMISSIONS.NONE; let permLevel = command.permission ?? 0;
for (let param of args) { for (let param of args) {
if (command.endpoint) { if (command.endpoint) {
@ -122,7 +122,11 @@ export default new Event<"message">({
if (!hasPermission(message.member, permLevel)) { if (!hasPermission(message.member, permLevel)) {
const userPermLevel = getPermissionLevel(message.member); const userPermLevel = getPermissionLevel(message.member);
return message.channel.send( return message.channel.send(
`You don't have access to this command! Your permission level is \`${PermissionNames[userPermLevel]}\` (${userPermLevel}), but this command requires a permission level of \`${PermissionNames[permLevel]}\` (${permLevel}).` `You don't have access to this command! Your permission level is \`${getPermissionName(
userPermLevel
)}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName(
permLevel
)}\` (${permLevel}).`
); );
} }

View file

@ -2,13 +2,25 @@ import chalk from "chalk";
declare global { declare global {
var IS_DEV_MODE: boolean; var IS_DEV_MODE: boolean;
var PERMISSIONS: typeof PermissionsEnum;
interface Console { interface Console {
ready: (...data: any[]) => void; ready: (...data: any[]) => void;
} }
} }
enum PermissionsEnum {
NONE,
MOD,
ADMIN,
OWNER,
BOT_SUPPORT,
BOT_ADMIN,
BOT_OWNER
}
global.IS_DEV_MODE = process.argv[2] === "dev"; global.IS_DEV_MODE = process.argv[2] === "dev";
global.PERMISSIONS = PermissionsEnum;
const oldConsole = console; const oldConsole = console;