From 1621b2c2815f7458df19320361a4a5a41c9cf4d3 Mon Sep 17 00:00:00 2001 From: Max Isom Date: Sat, 29 Jan 2022 11:20:40 -0500 Subject: [PATCH] Add permissions system --- .../migration.sql | 19 ++++ .../migration.sql | 2 + schema.prisma | 2 +- src/bot.ts | 5 + src/commands/config.ts | 103 ++++++++++++++++++ src/commands/favorites.ts | 12 +- src/inversify.config.ts | 2 + src/utils/update-permissions-for-guild.ts | 44 ++++++++ 8 files changed, 184 insertions(+), 5 deletions(-) create mode 100644 migrations/20220129010359_remove_channel/migration.sql create mode 100644 migrations/20220129012310_add_role_id_column/migration.sql create mode 100644 src/commands/config.ts create mode 100644 src/utils/update-permissions-for-guild.ts diff --git a/migrations/20220129010359_remove_channel/migration.sql b/migrations/20220129010359_remove_channel/migration.sql new file mode 100644 index 0000000..38a5336 --- /dev/null +++ b/migrations/20220129010359_remove_channel/migration.sql @@ -0,0 +1,19 @@ +/* + Warnings: + + - You are about to drop the column `channel` on the `Setting` table. All the data in the column will be lost. + +*/ +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_Setting" ( + "guildId" TEXT NOT NULL PRIMARY KEY, + "playlistLimit" INTEGER NOT NULL DEFAULT 50, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); +INSERT INTO "new_Setting" ("createdAt", "guildId", "playlistLimit", "updatedAt") SELECT "createdAt", "guildId", "playlistLimit", "updatedAt" FROM "Setting"; +DROP TABLE "Setting"; +ALTER TABLE "new_Setting" RENAME TO "Setting"; +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; diff --git a/migrations/20220129012310_add_role_id_column/migration.sql b/migrations/20220129012310_add_role_id_column/migration.sql new file mode 100644 index 0000000..d46c94c --- /dev/null +++ b/migrations/20220129012310_add_role_id_column/migration.sql @@ -0,0 +1,2 @@ +-- AlterTable +ALTER TABLE "Setting" ADD COLUMN "roleId" TEXT; diff --git a/schema.prisma b/schema.prisma index bf61bd7..66ea3b7 100644 --- a/schema.prisma +++ b/schema.prisma @@ -25,8 +25,8 @@ model KeyValueCache { model Setting { guildId String @id - channel String? playlistLimit Int @default(50) + roleId String? createdAt DateTime @default(now()) updatedAt DateTime @updatedAt } diff --git a/src/bot.ts b/src/bot.ts index 3ba15bc..d6bf02f 100644 --- a/src/bot.ts +++ b/src/bot.ts @@ -13,6 +13,7 @@ import Config from './services/config.js'; import {generateDependencyReport} from '@discordjs/voice'; import {REST} from '@discordjs/rest'; import {Routes} from 'discord-api-types/v9'; +import updatePermissionsForGuild from './utils/update-permissions-for-guild.js'; @injectable() export default class { @@ -146,6 +147,10 @@ export default class { ); } + // Update permissions + spinner.text = '📡 updating permissions...'; + await Promise.all(this.client.guilds.cache.map(async guild => updatePermissionsForGuild(guild))); + spinner.succeed(`Ready! Invite the bot with https://discordapp.com/oauth2/authorize?client_id=${this.client.user?.id ?? ''}&scope=bot%20applications.commands&permissions=2184236096`); }); diff --git a/src/commands/config.ts b/src/commands/config.ts new file mode 100644 index 0000000..65aa36b --- /dev/null +++ b/src/commands/config.ts @@ -0,0 +1,103 @@ +import {SlashCommandBuilder} from '@discordjs/builders'; +import {CommandInteraction, MessageEmbed} from 'discord.js'; +import {injectable} from 'inversify'; +import {prisma} from '../utils/db.js'; +import updatePermissionsForGuild from '../utils/update-permissions-for-guild.js'; +import Command from './index.js'; + +@injectable() +export default class implements Command { + public readonly slashCommand = new SlashCommandBuilder() + .setName('config') + .setDescription('configure bot settings') + .addSubcommand(subcommand => subcommand + .setName('set-playlist-limit') + .setDescription('set the maximum number of tracks that can be added from a playlist') + .addIntegerOption(option => option + .setName('limit') + .setDescription('maximum number of tracks') + .setRequired(true))) + .addSubcommand(subcommand => subcommand + .setName('set-role') + .setDescription('set the role that is allowed to use the bot') + .addRoleOption(option => option + .setName('role') + .setDescription('allowed role') + .setRequired(true))) + .addSubcommand(subcommand => subcommand + .setName('get') + .setDescription('show all settings')); + + async execute(interaction: CommandInteraction) { + switch (interaction.options.getSubcommand()) { + case 'set-playlist-limit': { + const limit = interaction.options.getInteger('limit')!; + + if (limit < 1) { + throw new Error('invalid limit'); + } + + await prisma.setting.update({ + where: { + guildId: interaction.guild!.id, + }, + data: { + playlistLimit: limit, + }, + }); + + await interaction.reply('👍 limit updated'); + + break; + } + + case 'set-role': { + const role = interaction.options.getRole('role')!; + + await prisma.setting.update({ + where: { + guildId: interaction.guild!.id, + }, + data: { + roleId: role.id, + }, + }); + + await updatePermissionsForGuild(interaction.guild!); + + await interaction.reply('👍 role updated'); + + break; + } + + case 'get': { + const embed = new MessageEmbed().setTitle('Config'); + + const config = await prisma.setting.findUnique({where: {guildId: interaction.guild!.id}}); + + if (!config) { + throw new Error('no config found'); + } + + const settingsToShow = { + 'Playlist Limit': config.playlistLimit, + Role: config.roleId ? `<@&${config.roleId}>` : 'not set', + }; + + let description = ''; + for (const [key, value] of Object.entries(settingsToShow)) { + description += `**${key}**: ${value}\n`; + } + + embed.setDescription(description); + + await interaction.reply({embeds: [embed]}); + + break; + } + + default: + throw new Error('unknown subcommand'); + } + } +} diff --git a/src/commands/favorites.ts b/src/commands/favorites.ts index b15a5c9..98f1ba9 100644 --- a/src/commands/favorites.ts +++ b/src/commands/favorites.ts @@ -75,6 +75,7 @@ export default class implements Command { } async handleAutocompleteInteraction(interaction: AutocompleteInteraction) { + const subcommand = interaction.options.getSubcommand(); const query = interaction.options.getString('name')!.trim(); const favorites = await prisma.favoriteQuery.findMany({ @@ -83,13 +84,16 @@ export default class implements Command { }, }); - const names = favorites.map(favorite => favorite.name); + let results = query === '' ? favorites : favorites.filter(f => f.name.startsWith(query)); - const results = query === '' ? names : names.filter(name => name.startsWith(query)); + if (subcommand === 'remove') { + // Only show favorites that user is allowed to remove + results = interaction.member?.user.id === interaction.guild?.ownerId ? results : results.filter(r => r.authorId === interaction.member!.user.id); + } await interaction.respond(results.map(r => ({ - name: r, - value: r, + name: r.name, + value: r.name, }))); } diff --git a/src/inversify.config.ts b/src/inversify.config.ts index f423614..d29d3de 100644 --- a/src/inversify.config.ts +++ b/src/inversify.config.ts @@ -15,6 +15,7 @@ import GetSongs from './services/get-songs.js'; // Comands import Command from './commands'; import Clear from './commands/clear.js'; +import Config from './commands/config.js'; import Disconnect from './commands/disconnect.js'; import Favorites from './commands/favorites.js'; import ForwardSeek from './commands/fseek.js'; @@ -54,6 +55,7 @@ container.bind(TYPES.Services.AddQueryToQueue).to(AddQueryToQue // Commands [ Clear, + Config, Disconnect, Favorites, ForwardSeek, diff --git a/src/utils/update-permissions-for-guild.ts b/src/utils/update-permissions-for-guild.ts new file mode 100644 index 0000000..ca7c427 --- /dev/null +++ b/src/utils/update-permissions-for-guild.ts @@ -0,0 +1,44 @@ +import {ApplicationCommandPermissionData, Guild} from 'discord.js'; +import {prisma} from './db.js'; + +const COMMANDS_TO_LIMIT_TO_GUILD_OWNER = ['config']; + +const updatePermissionsForGuild = async (guild: Guild) => { + const settings = await prisma.setting.findUnique({ + where: { + guildId: guild.id, + }, + }); + + if (!settings) { + throw new Error('could not find settings for guild'); + } + + const permissions: ApplicationCommandPermissionData[] = [ + { + id: guild.ownerId, + type: 'USER', + permission: true, + }, + { + id: guild.roles.everyone.id, + type: 'ROLE', + permission: false, + }, + ]; + const commands = await guild.commands.fetch(); + + await guild.commands.permissions.set({fullPermissions: commands.map(command => ({ + id: command.id, + permissions: COMMANDS_TO_LIMIT_TO_GUILD_OWNER.includes(command.name) ? permissions : [ + ...permissions, + ...(settings.roleId ? [{ + id: settings.roleId, + type: 'ROLE' as const, + permission: true, + }] : []), + ], + }))}); +}; + +export default updatePermissionsForGuild;