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 {botHasPermission, clean} from "../core/libd";
import {Config, Storage} from "../core/structures";
import {PermissionNames, getPermissionLevel} from "../core/permissions";
import {getPermissionLevel, getPermissionName} from "../core/permissions";
import {Permissions} from "discord.js";
import * as discord from "discord.js";
import {logs} from "../globals";
@ -30,14 +30,14 @@ export default new Command({
}
const permLevel = getPermissionLevel($.member);
$.channel.send(
`${$.author.toString()}, your permission level is \`${PermissionNames[permLevel]}\` (${permLevel}).`
`${$.author.toString()}, your permission level is \`${getPermissionName(permLevel)}\` (${permLevel}).`
);
},
subcommands: {
set: new Command({
description: "Set different per-guild settings for the bot.",
run: "You have to specify the option you want to set.",
permission: Command.PERMISSIONS.ADMIN,
permission: PERMISSIONS.ADMIN,
subcommands: {
prefix: new Command({
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({
description: 'Requests a debug log with the "info" verbosity level.',
permission: Command.PERMISSIONS.BOT_SUPPORT,
permission: PERMISSIONS.BOT_SUPPORT,
async run($) {
$.channel.send(getLogBuffer("info"));
},
@ -82,7 +82,7 @@ export default new Command({
}),
status: new Command({
description: "Changes the bot's status.",
permission: Command.PERMISSIONS.BOT_SUPPORT,
permission: PERMISSIONS.BOT_SUPPORT,
async run($) {
$.channel.send("Setting status to `online`...");
},
@ -101,7 +101,7 @@ export default new Command({
}),
purge: new Command({
description: "Purges bot messages.",
permission: Command.PERMISSIONS.BOT_SUPPORT,
permission: PERMISSIONS.BOT_SUPPORT,
async run($) {
if ($.message.channel instanceof discord.DMChannel) {
return;
@ -142,7 +142,7 @@ export default new Command({
eval: new Command({
description: "Evaluate 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.
async run({args, author, channel, client, guild, member, message}) {
try {
@ -158,7 +158,7 @@ export default new Command({
}),
nick: new Command({
description: "Change the bot's nickname.",
permission: Command.PERMISSIONS.BOT_SUPPORT,
permission: PERMISSIONS.BOT_SUPPORT,
async run($) {
const nickName = $.args.join(" ");
await $.guild?.me?.setNickname(nickName);
@ -169,7 +169,7 @@ export default new Command({
}),
guilds: new Command({
description: "Shows a list of all guilds the bot is a member of.",
permission: Command.PERMISSIONS.BOT_SUPPORT,
permission: PERMISSIONS.BOT_SUPPORT,
async run($) {
const guildList = $.client.guilds.cache.array().map((e) => e.name);
$.channel.send(guildList);
@ -177,7 +177,7 @@ export default new Command({
}),
activity: new Command({
description: "Set the activity of the bot.",
permission: Command.PERMISSIONS.BOT_SUPPORT,
permission: PERMISSIONS.BOT_SUPPORT,
usage: "<type> <string>",
async run($) {
$.client.user?.setActivity(".help", {

View file

@ -1,7 +1,7 @@
import Command from "../core/command";
import {toTitleCase} from "../core/lib";
import {loadableCommands, categories} from "../core/command";
import {PermissionNames} from "../core/permissions";
import {getPermissionName} from "../core/permissions";
export default new Command({
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;
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 invalid = false;
@ -135,7 +135,9 @@ export default new Command({
}
$.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}
);
}

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.',
endpoint: false,
usage: "",
permission: null,
permission: -1,
aliases: [],
async run($) {
// 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".',
endpoint: false,
usage: "",
permission: null,
permission: -1,
aliases: [],
async run($) {
// 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.',
endpoint: false,
usage: "",
permission: null,
permission: -1,
async run($) {
// 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.',
endpoint: false,
usage: "",
permission: null,
permission: -1,
async run($) {
// 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\".",
endpoint: false,
usage: "",
permission: null,
permission: -1,
async run($) {
// code
}

View file

@ -1,7 +1,6 @@
import {parseVars} from "./libd";
import {Collection} 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 glob from "glob";
@ -19,7 +18,7 @@ interface CommandOptions {
description?: string;
endpoint?: boolean;
usage?: string;
permission?: PERMISSIONS | null;
permission?: number;
aliases?: string[];
run?: (($: CommandMenu) => Promise<any>) | string;
subcommands?: {[key: string]: Command};
@ -40,7 +39,7 @@ export default class Command {
public readonly description: string;
public readonly endpoint: boolean;
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 originalCommandName: string | null; // If the command is an alias, what's the original name?
public run: (($: CommandMenu) => Promise<any>) | string;
@ -49,13 +48,12 @@ export default class Command {
public number: Command | null;
public any: Command | null;
public static readonly TYPES = TYPES;
public static readonly PERMISSIONS = PERMISSIONS;
constructor(options?: CommandOptions) {
this.description = options?.description || "No description.";
this.endpoint = options?.endpoint || false;
this.usage = options?.usage || "";
this.permission = options?.permission ?? null;
this.permission = options?.permission ?? -1;
this.aliases = options?.aliases ?? [];
this.originalCommandName = null;
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";
export enum PERMISSIONS {
NONE,
MOD,
ADMIN,
OWNER,
BOT_SUPPORT,
BOT_ADMIN,
BOT_OWNER
interface PermissionLevel {
name: string;
check: (user: User, member: GuildMember | null) => boolean;
}
export const PermissionNames = [
"User",
"Moderator",
"Administrator",
"Server Owner",
"Bot Support",
"Bot Admin",
"Bot Owner"
];
// Here is where you enter in the functions that check for permissions.
const PermissionChecker: ((member: GuildMember) => boolean)[] = [
// NONE //
() => true,
// MOD //
(member) =>
member.hasPermission(Permissions.FLAGS.MANAGE_ROLES) ||
member.hasPermission(Permissions.FLAGS.MANAGE_MESSAGES) ||
member.hasPermission(Permissions.FLAGS.KICK_MEMBERS) ||
member.hasPermission(Permissions.FLAGS.BAN_MEMBERS),
// ADMIN //
(member) => member.hasPermission(Permissions.FLAGS.ADMINISTRATOR),
// OWNER //
(member) => member.guild.ownerID === member.id,
// BOT_SUPPORT //
(member) => Config.support.includes(member.id),
// BOT_ADMIN //
(member) => Config.admins.includes(member.id),
// BOT_OWNER //
(member) => Config.owner === member.id
export const PermissionLevels: PermissionLevel[] = [
{
// NONE //
name: "User",
check: () => true
},
{
// MOD //
name: "Moderator",
check: (_, member) =>
!!member &&
(member.hasPermission(Permissions.FLAGS.MANAGE_ROLES) ||
member.hasPermission(Permissions.FLAGS.MANAGE_MESSAGES) ||
member.hasPermission(Permissions.FLAGS.KICK_MEMBERS) ||
member.hasPermission(Permissions.FLAGS.BAN_MEMBERS))
},
{
// ADMIN //
name: "Administrator",
check: (_, member) => !!member && member.hasPermission(Permissions.FLAGS.ADMINISTRATOR)
},
{
// OWNER //
name: "Server Owner",
check: (_, member) => !!member && member.guild.ownerID === member.id
},
{
// BOT_SUPPORT //
name: "Bot Support",
check: (user) => Config.support.includes(user.id)
},
{
// BOT_ADMIN //
name: "Bot Admin",
check: (user) => Config.admins.includes(user.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.
const length = Object.keys(PERMISSIONS).length / 2;
const length = PermissionLevels.length;
export function hasPermission(member: GuildMember, permission: PERMISSIONS): boolean {
for (let i = length - 1; i >= permission; i--) if (PermissionChecker[i](member)) return true;
export function hasPermission(member: GuildMember, permission: number): boolean {
for (let i = length - 1; i >= permission; i--) if (PermissionLevels[i].check(member.user, member)) return true;
return false;
}
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;
}
// Length Checking
(() => {
const lenNames = PermissionNames.length;
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!`
);
})();
export function getPermissionName(level: number) {
if (level > length || length < 0) return "N/A";
else return PermissionLevels[level].name;
}

View file

@ -1,6 +1,6 @@
import Event from "../core/event";
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 {getPrefix} from "../core/structures";
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!`);
const params: any[] = [];
let isEndpoint = false;
let permLevel = command.permission ?? Command.PERMISSIONS.NONE;
let permLevel = command.permission ?? 0;
for (let param of args) {
if (command.endpoint) {
@ -122,7 +122,11 @@ export default new Event<"message">({
if (!hasPermission(message.member, permLevel)) {
const userPermLevel = getPermissionLevel(message.member);
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 {
var IS_DEV_MODE: boolean;
var PERMISSIONS: typeof PermissionsEnum;
interface Console {
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.PERMISSIONS = PermissionsEnum;
const oldConsole = console;