mirror of
https://github.com/keanuplayz/TravBot-v3.git
synced 2024-08-15 02:33:12 +00:00
Added a permissions system
This commit is contained in:
parent
2488239598
commit
bf84c2970d
8 changed files with 139 additions and 81 deletions
|
@ -23,6 +23,7 @@ This list starts from `src`/`dist`.
|
|||
- `core/event`: Contains the class used to instantiate events.
|
||||
- `core/storage`: Exports an object which handles everything related to files.
|
||||
- `core/wrappers`: Contains classes that wrap around values and provide extra functionality.
|
||||
- `core/permissions`: The file containing everything related to permissions.
|
||||
|
||||
# Design Decisions
|
||||
- All top-level files (relative to `src`/`dist`) should ideally be independent, one-time use scripts. This helps separate code that just initializes once and reusable code that forms the bulk of the main program itself. That's why all the file searching and loading commands/events will be done in `index`.
|
||||
|
|
|
@ -1,25 +1,7 @@
|
|||
import Command from "../core/command";
|
||||
import {CommonLibrary, logs} from "../core/lib";
|
||||
import {Config, Storage} from "../core/structures";
|
||||
import {Permissions} from "discord.js";
|
||||
|
||||
function authenticate($: CommonLibrary, customMessage = ""): boolean
|
||||
{
|
||||
const hasAccess = Config.mechanics.includes($.author.id) || ($.member?.hasPermission(Permissions.FLAGS.ADMINISTRATOR) || false);
|
||||
|
||||
if(!hasAccess)
|
||||
{
|
||||
if(customMessage !== "")
|
||||
$.channel.send(customMessage);
|
||||
else
|
||||
{
|
||||
$.channel.send(`${$.author.toString()}, you are not a server admin or one of the bot's mechanics. If you have access to the server files, add yourself to it manually in \`data/config.json\`. Your user ID should now be logged in the console.`);
|
||||
$.debug($.author.id);
|
||||
}
|
||||
}
|
||||
|
||||
return hasAccess;
|
||||
}
|
||||
import {PermissionNames, getPermissionLevel} from "../core/permissions";
|
||||
|
||||
function getLogBuffer(type: string)
|
||||
{
|
||||
|
@ -33,25 +15,17 @@ export default new Command({
|
|||
description: "An all-in-one command to do admin stuff. You need to be either an admin of the server or one of the bot's mechanics to use this command.",
|
||||
async run($: CommonLibrary): Promise<any>
|
||||
{
|
||||
const admin = $.member?.hasPermission(Permissions.FLAGS.ADMINISTRATOR) || false;
|
||||
const mechanic = Config.mechanics.includes($.author.id);
|
||||
let status = "";
|
||||
|
||||
if(admin && mechanic)
|
||||
status = "a server admin and one of the bot's mechanics";
|
||||
else if(admin)
|
||||
status = "a server admin";
|
||||
else if(mechanic)
|
||||
status = "one of the bot's mechanics";
|
||||
|
||||
if(authenticate($))
|
||||
$.channel.send(`${$.author.toString()}, you are ${status}, meaning you can use this command.`);
|
||||
if(!$.member)
|
||||
return $.channel.send("Couldn't find a member object for you! Did you make sure you used this in a server?");
|
||||
const permLevel = getPermissionLevel($.member);
|
||||
$.channel.send(`${$.author.toString()}, your permission level is \`${PermissionNames[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,
|
||||
subcommands:
|
||||
{
|
||||
prefix: new Command({
|
||||
|
@ -59,22 +33,16 @@ export default new Command({
|
|||
usage: "(<prefix>)",
|
||||
async run($: CommonLibrary): Promise<any>
|
||||
{
|
||||
if(authenticate($))
|
||||
{
|
||||
Storage.getGuild($.guild?.id || "N/A").prefix = null;
|
||||
Storage.save();
|
||||
$.channel.send(`The custom prefix for this guild has been removed. My prefix is now back to \`${Config.prefix}\`.`);
|
||||
}
|
||||
Storage.getGuild($.guild?.id || "N/A").prefix = null;
|
||||
Storage.save();
|
||||
$.channel.send(`The custom prefix for this guild has been removed. My prefix is now back to \`${Config.prefix}\`.`);
|
||||
},
|
||||
any: new Command({
|
||||
async run($: CommonLibrary): Promise<any>
|
||||
{
|
||||
if(authenticate($))
|
||||
{
|
||||
Storage.getGuild($.guild?.id || "N/A").prefix = $.args[0];
|
||||
Storage.save();
|
||||
$.channel.send(`The custom prefix for this guild is now \`${$.args[0]}\`.`);
|
||||
}
|
||||
Storage.getGuild($.guild?.id || "N/A").prefix = $.args[0];
|
||||
Storage.save();
|
||||
$.channel.send(`The custom prefix for this guild is now \`${$.args[0]}\`.`);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
@ -82,24 +50,21 @@ export default new Command({
|
|||
}),
|
||||
diag: new Command({
|
||||
description: "Requests a debug log with the \"info\" verbosity level.",
|
||||
permission: Command.PERMISSIONS.BOT_SUPPORT,
|
||||
async run($: CommonLibrary): Promise<any>
|
||||
{
|
||||
if(authenticate($))
|
||||
$.channel.send(getLogBuffer("info"));
|
||||
$.channel.send(getLogBuffer("info"));
|
||||
},
|
||||
any: new Command({
|
||||
description: `Select a verbosity to listen to. Available levels: \`[${Object.keys(logs)}]\``,
|
||||
async run($: CommonLibrary): Promise<any>
|
||||
{
|
||||
if(authenticate($))
|
||||
{
|
||||
const type = $.args[0];
|
||||
|
||||
if(type in logs)
|
||||
$.channel.send(getLogBuffer(type));
|
||||
else
|
||||
$.channel.send(`Couldn't find a verbosity level named \`${type}\`! The available types are \`[${Object.keys(logs)}]\`.`);
|
||||
}
|
||||
const type = $.args[0];
|
||||
|
||||
if(type in logs)
|
||||
$.channel.send(getLogBuffer(type));
|
||||
else
|
||||
$.channel.send(`Couldn't find a verbosity level named \`${type}\`! The available types are \`[${Object.keys(logs)}]\`.`);
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
import Command from "../core/command";
|
||||
import {CommonLibrary} from "../core/lib";
|
||||
import {loadCommands, categories} from "../core/command";
|
||||
import {PermissionNames} from "../core/permissions";
|
||||
|
||||
const types = ["user", "number", "any"];
|
||||
|
||||
|
@ -42,6 +43,7 @@ export default new Command({
|
|||
if(!command || header === "test")
|
||||
return $.channel.send(`No command found by the name \`${header}\`!`);
|
||||
|
||||
let permLevel = command.permission ?? Command.PERMISSIONS.NONE;
|
||||
let usage = command.usage;
|
||||
let invalid = false;
|
||||
|
||||
|
@ -65,6 +67,7 @@ export default new Command({
|
|||
}
|
||||
|
||||
command = command.get(param);
|
||||
permLevel = command.permission ?? permLevel;
|
||||
}
|
||||
|
||||
if(invalid)
|
||||
|
@ -98,7 +101,7 @@ export default new Command({
|
|||
else
|
||||
append = `Usage: \`${header} ${usage}\``;
|
||||
|
||||
$.channel.send(`Command: \`${header}\`\nDescription: ${command.description}\n${append}`, {split: true});
|
||||
$.channel.send(`Command: \`${header}\`\nPermission Required: \`${PermissionNames[permLevel]}\` (${permLevel})\nDescription: ${command.description}\n${append}`, {split: true});
|
||||
}
|
||||
})
|
||||
});
|
|
@ -1,19 +1,15 @@
|
|||
import $, {isType, parseVars, CommonLibrary} from "./lib";
|
||||
import {Collection} from "discord.js";
|
||||
import {generateHandler} from "./storage";
|
||||
import {existsSync, writeFile} from "fs";
|
||||
import {promises as ffs} from "fs";
|
||||
|
||||
// Permission levels starting from zero then increasing, allowing for numerical comparisons.
|
||||
// Note: For my bot, there really isn't much purpose to doing so, as it's just one command. And plus, if you're doing stuff like moderation commands, it's probably better to make a permissions system that allows for you to separate permissions into different trees. After all, it'd be a really bad idea to allow a bot mechanic to ban users.
|
||||
//enum PERMISSIONS {NONE, ADMIN, MECHANIC}
|
||||
import {promises as ffs, existsSync, writeFile} from "fs";
|
||||
import {PERMISSIONS} from "./permissions";
|
||||
|
||||
interface CommandOptions
|
||||
{
|
||||
description?: string;
|
||||
endpoint?: boolean;
|
||||
usage?: string;
|
||||
//permissions?: number;
|
||||
permission?: PERMISSIONS;
|
||||
run?: Function|string;
|
||||
subcommands?: {[key: string]: Command};
|
||||
user?: Command;
|
||||
|
@ -28,22 +24,22 @@ export default class Command
|
|||
public readonly description: string;
|
||||
public readonly endpoint: boolean;
|
||||
public readonly usage: string;
|
||||
//public readonly permissions: number;
|
||||
public readonly permission: PERMISSIONS|null;
|
||||
private run: Function|string;
|
||||
public subcommands: {[key: string]: Command}|null;
|
||||
public user: Command|null;
|
||||
public number: Command|null;
|
||||
public any: Command|null;
|
||||
//public static readonly PERMISSIONS = PERMISSIONS;
|
||||
[key: string]: any; // Allow for dynamic indexing. The CommandOptions interface will still prevent users from adding unused properties though.
|
||||
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.permissions = options?.permissions || Command.PERMISSIONS.NONE;
|
||||
this.permission = options?.permission ?? null;
|
||||
this.run = options?.run || "No action was set on this command!";
|
||||
this.subcommands = options?.subcommands || null;
|
||||
this.user = options?.user || null;
|
||||
|
@ -115,13 +111,6 @@ export default class Command
|
|||
}
|
||||
}
|
||||
|
||||
/*export function hasPermission(member: GuildMember, permission: number): boolean
|
||||
{
|
||||
const length = Object.keys(PERMISSIONS).length / 2;
|
||||
console.log(member, permission, length);
|
||||
return true;
|
||||
}*/
|
||||
|
||||
let commands: Collection<string, Command>|null = null;
|
||||
export const categories: Collection<string, string[]> = new Collection();
|
||||
|
||||
|
|
76
src/core/permissions.ts
Normal file
76
src/core/permissions.ts
Normal file
|
@ -0,0 +1,76 @@
|
|||
import {GuildMember, Permissions} from "discord.js";
|
||||
import {Config} from "./structures";
|
||||
import $ from "./lib";
|
||||
|
||||
export enum PERMISSIONS {NONE, MOD, ADMIN, OWNER, BOT_SUPPORT, BOT_ADMIN, BOT_OWNER};
|
||||
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: GuildMember) =>
|
||||
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: GuildMember) => member.hasPermission(Permissions.FLAGS.ADMINISTRATOR),
|
||||
|
||||
// OWNER //
|
||||
(member: GuildMember) => member.guild.ownerID === member.id,
|
||||
|
||||
// BOT_SUPPORT //
|
||||
(member: GuildMember) => Config.support.includes(member.id),
|
||||
|
||||
// BOT_ADMIN //
|
||||
(member: GuildMember) => Config.admins.includes(member.id),
|
||||
|
||||
// BOT_OWNER //
|
||||
(member: GuildMember) => Config.owner === member.id
|
||||
];
|
||||
|
||||
// After checking the lengths of these three objects, use this as the length for consistency.
|
||||
const length = Object.keys(PERMISSIONS).length / 2;
|
||||
|
||||
export function hasPermission(member: GuildMember, permission: PERMISSIONS): boolean
|
||||
{
|
||||
if(permission === PERMISSIONS.NONE)
|
||||
return true;
|
||||
|
||||
for(let i = length-1; i >= permission; i--)
|
||||
{
|
||||
const condition = PermissionChecker[i](member);
|
||||
|
||||
if(condition)
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export function getPermissionLevel(member: GuildMember): number
|
||||
{
|
||||
for(let i = length-1; i >= 0; i--)
|
||||
{
|
||||
const condition = PermissionChecker[i](member);
|
||||
|
||||
if(condition)
|
||||
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)
|
||||
$.error(`Permission object lengths aren't equal! Enum Length (${length}), Names Length (${lenNames}), and Functions Length (${lenChecker}). This WILL cause problems!`);
|
||||
})()
|
|
@ -6,14 +6,18 @@ class ConfigStructure extends GenericStructure
|
|||
{
|
||||
public token: string;
|
||||
public prefix: string;
|
||||
public mechanics: string[];
|
||||
public owner: string;
|
||||
public admins: string[];
|
||||
public support: string[];
|
||||
|
||||
constructor(data: GenericJSON)
|
||||
{
|
||||
super("config");
|
||||
this.token = select(data.token, "<ENTER YOUR TOKEN HERE>", String);
|
||||
this.prefix = select(data.prefix, "$", String);
|
||||
this.mechanics = select(data.mechanics, [], String, true);
|
||||
this.owner = select(data.owner, "", String);
|
||||
this.admins = select(data.admins, [], String, true);
|
||||
this.support = select(data.support, [], String, true);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
import Event from "../core/event";
|
||||
import Command from "../core/command";
|
||||
import Command, {loadCommands} from "../core/command";
|
||||
import {hasPermission, getPermissionLevel, PermissionNames} from "../core/permissions";
|
||||
import $ from "../core/lib";
|
||||
import {Message, Permissions, Collection} from "discord.js";
|
||||
import {Config, Storage} from "../core/structures";
|
||||
import {loadCommands} from "../core/command";
|
||||
|
||||
// It's a rather hacky solution, but since there's no top-level await, I just have to make the loading conditional.
|
||||
let commands: Collection<string, Command>|null = null;
|
||||
|
@ -47,6 +47,7 @@ export default new Event({
|
|||
if(!command) return $.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;
|
||||
|
||||
for(let param of args)
|
||||
{
|
||||
|
@ -60,6 +61,7 @@ export default new Event({
|
|||
|
||||
const type = command.resolve(param);
|
||||
command = command.get(param);
|
||||
permLevel = command.permission ?? permLevel;
|
||||
|
||||
if(type === Command.TYPES.USER)
|
||||
{
|
||||
|
@ -73,6 +75,13 @@ export default new Event({
|
|||
params.push(param);
|
||||
}
|
||||
|
||||
if(!message.member)
|
||||
return $.warn("This command was likely called from a DM channel meaning the member object is null.");
|
||||
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}).`);
|
||||
}
|
||||
if(isEndpoint)
|
||||
return message.channel.send("Too many arguments!");
|
||||
|
||||
|
|
19
src/setup.ts
19
src/setup.ts
|
@ -18,8 +18,16 @@ const prompts = [{
|
|||
default: "$"
|
||||
}, {
|
||||
type: "input",
|
||||
name: "mechanics",
|
||||
message: "Enter a list of bot mechanics (by their IDs) separated by spaces."
|
||||
name: "owner",
|
||||
message: "Enter the owner's user ID here."
|
||||
}, {
|
||||
type: "input",
|
||||
name: "admins",
|
||||
message: "Enter a list of bot admins (by their IDs) separated by spaces."
|
||||
}, {
|
||||
type: "input",
|
||||
name: "support",
|
||||
message: "Enter a list of bot troubleshooters (by their IDs) separated by spaces."
|
||||
}];
|
||||
|
||||
export default {
|
||||
|
@ -31,8 +39,11 @@ export default {
|
|||
Storage.open("data");
|
||||
Config.token = answers.token as string;
|
||||
Config.prefix = answers.prefix as string;
|
||||
const mechanics = (answers.mechanics as string);
|
||||
Config.mechanics = mechanics !== "" ? mechanics.split(" ") : [];
|
||||
Config.owner = answers.owner as string;
|
||||
const admins = (answers.admins as string);
|
||||
Config.admins = admins !== "" ? admins.split(" ") : [];
|
||||
const support = (answers.support as string);
|
||||
Config.support = support !== "" ? support.split(" ") : [];
|
||||
Config.save();
|
||||
}
|
||||
},
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue