diff --git a/permissions.json b/permissions.json new file mode 100644 index 0000000..0637a08 --- /dev/null +++ b/permissions.json @@ -0,0 +1 @@ +[] \ No newline at end of file diff --git a/roles.json b/roles.json new file mode 100644 index 0000000..9e26dfe --- /dev/null +++ b/roles.json @@ -0,0 +1 @@ +{} \ No newline at end of file diff --git a/src/app.ts b/src/app.ts index e1371b2..007d1ee 100644 --- a/src/app.ts +++ b/src/app.ts @@ -1,8 +1,11 @@ -import { ActivityType, Client, Events, GatewayIntentBits } from "discord.js"; +import { ActivityType, Client, Events, GatewayIntentBits, PermissionsBitField } from "discord.js"; import { youtube, token } from '../config.json'; import commands from "./commands"; import YTDlpWrap from "yt-dlp-wrap"; import { Embeds } from "./utils/embeds"; +import { permissionManager } from "./lib/permissions"; +import * as Style from "./utils/style"; + export const ytdl = new YTDlpWrap(youtube.binary_path); export const client = new Client({ @@ -29,13 +32,34 @@ client.once(Events.ClientReady, async client => { client.on(Events.InteractionCreate, async interaction => { if (interaction.isChatInputCommand()) { - const command = commands.find(command => command.data.name === interaction.commandName); + const command = commands.find(command => command.metadata.name === interaction.commandName); if (!command) { await Embeds.error(interaction, "Something went terribly wrong! Contact the developer."); return; } await interaction.deferReply(); - command.execute(client, interaction); + + const guild = interaction.guild; + if (!guild) { + await Embeds.error(interaction, `${Style.NAME} does not current support DMs!`); + return; + } + + const user = guild.members.cache.get(interaction.user.id); + if (!user) { + await Embeds.error(interaction, "You aren't a member of this server?? Something went wrong."); + return; + } + + if (command.permission && !user.permissions.has(PermissionsBitField.All, true)) { + const level = permissionManager.getPermissionLevel(guild.id, user.id); + if (command.permission > level) { + await Embeds.error(interaction, "You do not have permission to execute this command!"); + return; + } + } + + command.execute(client, user, interaction); } }); diff --git a/src/commands/admin/deploy.ts b/src/commands/admin/deploy.ts index 3caadcb..5c6f438 100644 --- a/src/commands/admin/deploy.ts +++ b/src/commands/admin/deploy.ts @@ -4,38 +4,39 @@ import { Embeds } from "../../utils/embeds"; import commands from ".."; import { token, development } from '../../../config.json'; +import { PermissionLevel } from "../../lib/permissions"; const MY_SNOWFLAKE = "140520164629151744"; export default { - data: new SlashCommandBuilder() + metadata: new SlashCommandBuilder() .setName('deploy') .setDescription('Deploys slash commands for the bot.'), - execute: async (_, interaction) => { - if (interaction.user.id === MY_SNOWFLAKE) { + permission: PermissionLevel.ADMIN, + execute: async (_, user, interaction) => { + if (user.id === MY_SNOWFLAKE) { const rest = new REST().setToken(token); + console.log(`Started refreshing ${commands.length} application (/) commands.`); + const data = await rest.put( + Routes.applicationCommands(development.application_id), + { body: commands.map(command => command.metadata) }, + ) as any[]; - // and deploy your commands! - (async () => { - try { - console.log(`Started refreshing ${commands.length} application (/) commands.`); - // The put method is used to fully refresh all commands in the guild with the current set - // Routes.applicationGuildCommands(DEVELOPMENT.APPLICATION_ID, DEVELOPMENT.GUILD_ID) - const data = await rest.put( - Routes.applicationCommands(development.application_id), - { body: commands.map(command => command.data) }, - ) as any[]; + let response = JSON.stringify(data, null, 2); + if (response.length > 2000) response = response.substring(0, 2000) + "..."; - console.log(`Successfully reloaded ${data.length} application (/) commands.`); - } catch (error) { - // And of course, make sure you catch and log any errors! - console.error(error); - } - })(); await Embeds.create() - .setTitle("Deployed commands") - .setDescription(`${commands.length} commands have been deployed.`) - .send(interaction); + .setTitle("Deployed commands") + .setAuthor({ name: `${commands.length} commands have been deployed.` }) + .setDescription(` + \`\`\`JSON + ${response} + \`\`\` + `) + .send(interaction); + + console.log(`Successfully reloaded ${data.length} application (/) commands.`); + } else await Embeds.error(interaction, "You do not have permission to execute this command!"); }, } \ No newline at end of file diff --git a/src/commands/admin/perms.ts b/src/commands/admin/perms.ts new file mode 100644 index 0000000..0bdc560 --- /dev/null +++ b/src/commands/admin/perms.ts @@ -0,0 +1,49 @@ +import { SlashCommandBuilder } from "discord.js"; +import { Command } from "../../lib/command"; +import { Embeds } from "../../utils/embeds"; +import { PermissionLevel, getNameFromPermissionLevel, permissionManager } from "../../lib/permissions"; + +export default { + metadata: new SlashCommandBuilder() + .setName('perms') + .setDescription('Changes internal bot permissions for a user') + .addSubcommand(subcommand => subcommand + .setName("set") + .setDescription("Sets a user's permission level") + .addUserOption(option => option.setName("user").setDescription("The user to set the permission level for").setRequired(true)) + .addStringOption(option => option.setName("level") + .setDescription("The permission level to set the user to") + .setChoices(...Object.keys(PermissionLevel).map(level => ({ name: level, value: level }))) + .setRequired(true)) + ) + .addSubcommand(subcommand => subcommand + .setName("view") + .setDescription("View a users permission level") + .addUserOption(option => option.setName("user").setDescription("The user to set the permission level for").setRequired(true)) + ) + .setDMPermission(false), + permission: PermissionLevel.ADMIN, + execute: async (_, user, interaction) => { + const subcommand = interaction.options.getSubcommand(); + if (subcommand == "set") { + const level = interaction.options.getString("level", true); + const target = interaction.options.getUser("user", true); + const permissionLevel = PermissionLevel[level as keyof typeof PermissionLevel]; + + permissionManager.setPermissionLevel(interaction.guild!.id, target.id, permissionLevel); + + await Embeds.create() + .setTitle("Permission Level") + .setDescription(`${target.toString()}'s permission level has been changed to \`${getNameFromPermissionLevel(permissionLevel)}\``) + .send(interaction); + } else if (subcommand == "view") { + const target = interaction.options.getUser("user", true); + const permissionLevel = permissionManager.getPermissionLevel(interaction.guild!.id, target.id); + + await Embeds.create() + .setTitle("Permission Level") + .setDescription(`${target.toString()}'s permission level is \`${getNameFromPermissionLevel(permissionLevel)}\``) + .send(interaction); + } + }, +} \ No newline at end of file diff --git a/src/commands/index.ts b/src/commands/index.ts index eea92a3..d8ba184 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -7,6 +7,7 @@ import Status from "./misc/status"; import Meme from "./silly/meme"; import Help from "./misc/help"; import Oobinate from "./silly/oob"; +import Perms from "./admin/perms"; -const commands = [ Help, Play, Skip, Queue, Stop, Meme, Oobinate, Status, Deploy ]; +const commands = [ Help, Play, Skip, Queue, Stop, Meme, Oobinate, Status, Deploy, Perms ]; export default commands; \ No newline at end of file diff --git a/src/commands/misc/help.ts b/src/commands/misc/help.ts index 5af329b..e7740a5 100644 --- a/src/commands/misc/help.ts +++ b/src/commands/misc/help.ts @@ -4,14 +4,14 @@ import { Embeds } from "../../utils/embeds"; import commands from ".."; export default { - data: new SlashCommandBuilder() + metadata: new SlashCommandBuilder() .setName('help') .setDescription('Lists all the available commands.'), - execute: async (_, interaction) => { + execute: async (_, user, interaction) => { await Embeds.create() .setTitle("Help") .setDescription(`There's a lot I can do for you. Here is a list of the avaliable commands:`) - .addFields(commands.map(command => ({ name: `/${command.data.name}`, value: command.data.description, inline: true }))) + .addFields(commands.map(command => ({ name: `/${command.metadata.name}`, value: command.metadata.description, inline: true }))) .send(interaction); }, } \ No newline at end of file diff --git a/src/commands/misc/status.ts b/src/commands/misc/status.ts index 1717883..603f210 100644 --- a/src/commands/misc/status.ts +++ b/src/commands/misc/status.ts @@ -7,10 +7,10 @@ import Time from "../../utils/time"; const startup = Date.now(); export default { - data: new SlashCommandBuilder() + metadata: new SlashCommandBuilder() .setName('status') .setDescription('Shows the current status for the bot.'), - execute: async (_, interaction) => { + execute: async (_, user, interaction) => { let sent = Date.now(); await Embeds.create() .setTitle("Status") diff --git a/src/commands/silly/meme.ts b/src/commands/silly/meme.ts index 355ba79..221ffc1 100644 --- a/src/commands/silly/meme.ts +++ b/src/commands/silly/meme.ts @@ -4,13 +4,13 @@ import axios from "axios"; import { Embeds } from "../../utils/embeds"; export default { - data: new SlashCommandBuilder() + metadata: new SlashCommandBuilder() .setName('meme') .setDescription('Create a meme with top and bottom text.') .addStringOption(option => option.setName("top").setRequired(true).setDescription("The text on the top of the meme.")) .addAttachmentOption(option => option.setName("image").setRequired(true).setDescription("The image to create a meme out of.")) .addStringOption(option => option.setName("bottom").setRequired(false).setDescription("The text on the bottom of the meme.")), - execute: async (client, interaction) => { + execute: async (client, user, interaction) => { let top = interaction.options.get("top", true); let bottom = interaction.options.get("bottom", false); let { attachment } = interaction.options.get("image", true); diff --git a/src/commands/silly/oob.ts b/src/commands/silly/oob.ts index 4804a82..ce959d5 100644 --- a/src/commands/silly/oob.ts +++ b/src/commands/silly/oob.ts @@ -3,11 +3,11 @@ import { Command } from "../../lib/command"; import { Embeds } from "../../utils/embeds"; export default { - data: new SlashCommandBuilder() + metadata: new SlashCommandBuilder() .setName('oobinate') .setDescription('Converts normal text into oob text.') .addStringOption(option => option.setName("text").setRequired(true).setDescription("The text to oobinate.")), - execute: async (client, interaction) => { + execute: async (client, user, interaction) => { let text = interaction.options.get("text", true); await Embeds.create() .setTitle("Oobinator") diff --git a/src/commands/voice/play.ts b/src/commands/voice/play.ts index f25fa2a..c9d4875 100644 --- a/src/commands/voice/play.ts +++ b/src/commands/voice/play.ts @@ -5,19 +5,18 @@ import YouTubeAPI from "../../utils/youtube"; import * as VoiceManager from "../../lib/voice"; export default { - data: new SlashCommandBuilder() + metadata: new SlashCommandBuilder() .setName("play") .setDescription("Play a song by search.") .addStringOption(option => option.setName("search").setRequired(true).setDescription("A valid YouTube search, picking the top result.")), - execute: async (_, interaction) => { + execute: async (_, user, interaction) => { - if (!interaction.guild || !interaction.member) { + if (!interaction.guild) { await Embeds.error(interaction, "You are not in a guild!"); return; } - let user = await interaction.guild.members.cache.get(interaction.member.user.id); - if (!user?.voice.channel) { + if (!user.voice.channel) { await Embeds.error(interaction, "You are not in a voice channel!"); return; } diff --git a/src/commands/voice/queue.ts b/src/commands/voice/queue.ts index 9ff6079..a27a927 100644 --- a/src/commands/voice/queue.ts +++ b/src/commands/voice/queue.ts @@ -6,18 +6,17 @@ import { Text } from "../../utils/misc"; import Time from "../../utils/time"; export default { - data: new SlashCommandBuilder() + metadata: new SlashCommandBuilder() .setName("queue") .setDescription("View the current song queue."), - execute: async (_, interaction) => { + execute: async (_, user, interaction) => { - if (!interaction.guild || !interaction.member) { + if (!interaction.guild) { await Embeds.error(interaction, "You are not in a guild!"); return; } - let user = await interaction.guild.members.cache.get(interaction.member.user.id); - if (!user?.voice.channel) { + if (!user.voice.channel) { await Embeds.error(interaction, "You are not in a voice channel!"); return; } diff --git a/src/commands/voice/skip.ts b/src/commands/voice/skip.ts index 844e13c..691d3bd 100644 --- a/src/commands/voice/skip.ts +++ b/src/commands/voice/skip.ts @@ -4,17 +4,16 @@ import { Embeds } from "../../utils/embeds"; import * as VoiceManager from "../../lib/voice"; export default { - data: new SlashCommandBuilder() + metadata: new SlashCommandBuilder() .setName("skip") .setDescription("Skip to the next song in the queue."), - execute: async (_, interaction) => { - if (!interaction.guild || !interaction.member) { + execute: async (_, user, interaction) => { + if (!interaction.guild) { await Embeds.error(interaction, "You are not in a guild!"); return; } - let user = await interaction.guild.members.cache.get(interaction.member.user.id); - if (!user?.voice.channel) { + if (!user.voice.channel) { await Embeds.error(interaction, "You are not in a voice channel!"); return; } diff --git a/src/commands/voice/stop.ts b/src/commands/voice/stop.ts index 3c1728b..2c310c0 100644 --- a/src/commands/voice/stop.ts +++ b/src/commands/voice/stop.ts @@ -4,18 +4,17 @@ import { Embeds } from "../../utils/embeds"; import * as VoiceManager from "../../lib/voice"; export default { - data: new SlashCommandBuilder() + metadata: new SlashCommandBuilder() .setName("stop") .setDescription("Disconnect from voice and clear the queue."), - execute: async (_, interaction) => { + execute: async (_, user, interaction) => { - if (!interaction.guild || !interaction.member) { + if (!interaction.guild) { await Embeds.error(interaction, "You are not in a guild!"); return; } - let user = await interaction.guild.members.cache.get(interaction.member.user.id); - if (!user?.voice.channel) { + if (!user.voice.channel) { await Embeds.error(interaction, "You are not in a voice channel!"); return; } diff --git a/src/lib/command.ts b/src/lib/command.ts index a665817..45f9aec 100644 --- a/src/lib/command.ts +++ b/src/lib/command.ts @@ -1,6 +1,8 @@ -import { Client, SlashCommandBuilder, ChatInputCommandInteraction } from "discord.js"; +import { ChatInputCommandInteraction, Client, GuildMember, SlashCommandBuilder } from "discord.js"; +import { PermissionLevel } from "./permissions"; export interface Command { - data: SlashCommandBuilder | Omit; - execute: (client: Client, interaction: ChatInputCommandInteraction) => void; + metadata: SlashCommandBuilder | Omit; + permission?: PermissionLevel; + execute: (client: Client, user: GuildMember, interaction: ChatInputCommandInteraction) => void; } \ No newline at end of file diff --git a/src/lib/permissions.ts b/src/lib/permissions.ts new file mode 100644 index 0000000..cc141ba --- /dev/null +++ b/src/lib/permissions.ts @@ -0,0 +1,71 @@ +import fs from "fs"; +import { get } from "http"; + +export enum PermissionLevel { + MEMBER = 0, + ADMIN = 1, +} + +export const getNameFromPermissionLevel = (level: PermissionLevel) => { + switch (level) { + case PermissionLevel.MEMBER: return "MEMBER"; + case PermissionLevel.ADMIN: return "ADMIN"; + } +} + +class PermissionManager { + private table: Map>; + + constructor() { + this.table = this.loadPermissions(); + } + + public setPermissionLevel(guildId: string, userId: string, level: PermissionLevel) { + let guildPermissions = this.table.get(guildId); + if (guildPermissions) { + guildPermissions.set(userId, level); + } else { + guildPermissions = new Map(); + guildPermissions.set(userId, level); + this.table.set(guildId, guildPermissions); + } + } + + public getPermissionLevel(guildId: string, userId: string) { + let guildPermissions = this.table.get(guildId); + if (guildPermissions) + return guildPermissions.get(userId) || PermissionLevel.MEMBER; + + return PermissionLevel.MEMBER; + } + + public savePermissions = () => { + let output: any = {}; + this.table.forEach((value, key) => { + output[key] = Array.from(value.entries()).map(([user, level]) => ({ user, level })); + }); + fs.writeFileSync("permissions.json", JSON.stringify(output)); + } + + private loadPermissions = () => { + if (!fs.existsSync("permissions.json")) { + fs.writeFileSync("permissions.json", JSON.stringify([])); + } + + // do flat file for now + const permissions: Map> = new Map(); + const data = JSON.parse(fs.readFileSync("permissions.json", "utf-8")) as any; + for (const guildId in data) { + const guildPermissions = new Map(); + for (const { user, level } of data[guildId]) { + guildPermissions.set(user, level); + } + permissions.set(guildId, guildPermissions); + } + + return permissions; + } + +} + +export const permissionManager = new PermissionManager(); \ No newline at end of file