diff options
| author | Kevin Kendzia <[email protected]> | 2022-05-14 02:44:14 +0200 |
|---|---|---|
| committer | GitHub <[email protected]> | 2022-05-13 19:44:14 -0500 |
| commit | eb2885b2061708415e46929d21bc5991724ce441 (patch) | |
| tree | e77496a2d2fdd249cc505ca1e06d7b17cce957ac /src | |
| parent | 1ef05aba9d2e692ef365721f725be2d2a4e464d9 (diff) | |
| download | muse-eb2885b2061708415e46929d21bc5991724ce441.tar.xz muse-eb2885b2061708415e46929d21bc5991724ce441.zip | |
fix command permission handling and push discord to v10 (#640)
Co-authored-by: Max Isom <[email protected]>
Diffstat (limited to 'src')
32 files changed, 138 insertions, 226 deletions
@@ -1,4 +1,4 @@ -import {Client, Collection, ExcludeEnum, PresenceStatusData, User} from 'discord.js'; +import {Client, Collection, PresenceStatusData, User} from 'discord.js'; import {inject, injectable} from 'inversify'; import ora from 'ora'; import {TYPES} from './types.js'; @@ -7,32 +7,27 @@ import Command from './commands/index.js'; import debug from './utils/debug.js'; import handleGuildCreate from './events/guild-create.js'; import handleVoiceStateUpdate from './events/voice-state-update.js'; -import handleGuildUpdate from './events/guild-update.js'; import errorMsg from './utils/error-msg.js'; import {isUserInVoice} from './utils/channels.js'; 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'; -import {ActivityTypes} from 'discord.js/typings/enums'; +import {Routes, ActivityType} from 'discord-api-types/v10'; +import registerCommandsOnGuild from './utils/register-commands-on-guild.js'; @injectable() export default class { private readonly client: Client; private readonly config: Config; - private readonly token: string; private readonly shouldRegisterCommandsOnBot: boolean; private readonly commandsByName!: Collection<string, Command>; private readonly commandsByButtonId!: Collection<string, Command>; constructor( @inject(TYPES.Client) client: Client, - @inject(TYPES.Config) config: Config, - ) { + @inject(TYPES.Config) config: Config) { this.client = client; this.config = config; - this.token = config.DISCORD_TOKEN; this.shouldRegisterCommandsOnBot = config.REGISTER_COMMANDS_ON_BOT; this.commandsByName = new Collection(); this.commandsByButtonId = new Collection(); @@ -61,12 +56,13 @@ export default class { } // Register event handlers + // eslint-disable-next-line complexity this.client.on('interactionCreate', async interaction => { try { if (interaction.isCommand()) { const command = this.commandsByName.get(interaction.commandName); - if (!command) { + if (!command || !interaction.isChatInputCommand()) { return; } @@ -76,7 +72,6 @@ export default class { } const requiresVC = command.requiresVC instanceof Function ? command.requiresVC(interaction) : command.requiresVC; - if (requiresVC && interaction.member && !isUserInVoice(interaction.guild, interaction.member.user as User)) { await interaction.reply({content: errorMsg('gotta be in a voice channel'), ephemeral: true}); return; @@ -111,9 +106,9 @@ export default class { // This can fail if the message was deleted, and we don't want to crash the whole bot try { - if ((interaction.isApplicationCommand() || interaction.isButton()) && (interaction.replied || interaction.deferred)) { + if ((interaction.isCommand() || interaction.isButton()) && (interaction.replied || interaction.deferred)) { await interaction.editReply(errorMsg(error as Error)); - } else if (interaction.isApplicationCommand() || interaction.isButton()) { + } else if (interaction.isCommand() || interaction.isButton()) { await interaction.reply({content: errorMsg(error as Error), ephemeral: true}); } } catch {} @@ -126,11 +121,9 @@ export default class { debug(generateDependencyReport()); // Update commands - const rest = new REST({version: '9'}).setToken(this.token); - + const rest = new REST({version: '10'}).setToken(this.config.DISCORD_TOKEN); if (this.shouldRegisterCommandsOnBot) { spinner.text = '📡 updating commands on bot...'; - await rest.put( Routes.applicationCommands(this.client.user!.id), {body: this.commandsByName.map(command => command.slashCommand.toJSON())}, @@ -140,10 +133,12 @@ export default class { await Promise.all([ ...this.client.guilds.cache.map(async guild => { - await rest.put( - Routes.applicationGuildCommands(this.client.user!.id, guild.id), - {body: this.commandsByName.map(command => command.slashCommand.toJSON())}, - ); + await registerCommandsOnGuild({ + rest, + guildId: guild.id, + applicationId: this.client.user!.id, + commands: this.commandsByName.map(c => c.slashCommand), + }); }), // Remove commands registered on bot (if they exist) rest.put(Routes.applicationCommands(this.client.user!.id), {body: []}), @@ -155,18 +150,14 @@ export default class { activities: [ { name: this.config.BOT_ACTIVITY, - type: this.config.BOT_ACTIVITY_TYPE as unknown as ExcludeEnum<typeof ActivityTypes, 'CUSTOM'>, + type: this.config.BOT_ACTIVITY_TYPE as unknown as Exclude<ActivityType, ActivityType.Custom>, url: this.config.BOT_ACTIVITY_URL === '' ? undefined : this.config.BOT_ACTIVITY_URL, }, ], status: this.config.BOT_STATUS as PresenceStatusData, }); - // 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=36700288`); + spinner.succeed(`Ready! Invite the bot with https://discordapp.com/oauth2/authorize?client_id=${this.client.user?.id ?? ''}&scope=bot%20applications.commands&permissions=36700160`); }); this.client.on('error', console.error); @@ -174,8 +165,6 @@ export default class { this.client.on('guildCreate', handleGuildCreate); this.client.on('voiceStateUpdate', handleVoiceStateUpdate); - this.client.on('guildUpdate', handleGuildUpdate); - - await this.client.login(this.token); + await this.client.login(); } } diff --git a/src/commands/clear.ts b/src/commands/clear.ts index 2b258cb..8de8764 100644 --- a/src/commands/clear.ts +++ b/src/commands/clear.ts @@ -1,5 +1,5 @@ import {inject, injectable} from 'inversify'; -import {CommandInteraction} from 'discord.js'; +import {ChatInputCommandInteraction} from 'discord.js'; import {SlashCommandBuilder} from '@discordjs/builders'; import {TYPES} from '../types.js'; import PlayerManager from '../managers/player.js'; @@ -19,7 +19,7 @@ export default class implements Command { this.playerManager = playerManager; } - public async execute(interaction: CommandInteraction) { + public async execute(interaction: ChatInputCommandInteraction) { this.playerManager.get(interaction.guild!.id).clear(); await interaction.reply('clearer than a field after a fresh harvest'); diff --git a/src/commands/config.ts b/src/commands/config.ts index 5ed286f..55dab71 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -1,8 +1,7 @@ import {SlashCommandBuilder} from '@discordjs/builders'; -import {CommandInteraction, MessageEmbed} from 'discord.js'; +import {ChatInputCommandInteraction, EmbedBuilder, PermissionFlagsBits} 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() @@ -10,6 +9,7 @@ export default class implements Command { public readonly slashCommand = new SlashCommandBuilder() .setName('config') .setDescription('configure bot settings') + .setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild.toString() as any) .addSubcommand(subcommand => subcommand .setName('set-playlist-limit') .setDescription('set the maximum number of tracks that can be added from a playlist') @@ -43,10 +43,10 @@ export default class implements Command { .setName('get') .setDescription('show all settings')); - async execute(interaction: CommandInteraction) { + async execute(interaction: ChatInputCommandInteraction) { switch (interaction.options.getSubcommand()) { case 'set-playlist-limit': { - const limit = interaction.options.getInteger('limit')!; + const limit: number = interaction.options.getInteger('limit')!; if (limit < 1) { throw new Error('invalid limit'); @@ -66,25 +66,6 @@ export default class implements Command { 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 'set-wait-after-queue-empties': { const delay = interaction.options.getInteger('delay')!; @@ -120,7 +101,7 @@ export default class implements Command { } case 'get': { - const embed = new MessageEmbed().setTitle('Config'); + const embed = new EmbedBuilder().setTitle('Config'); const config = await prisma.setting.findUnique({where: {guildId: interaction.guild!.id}}); diff --git a/src/commands/disconnect.ts b/src/commands/disconnect.ts index fd6b984..d9f025a 100644 --- a/src/commands/disconnect.ts +++ b/src/commands/disconnect.ts @@ -1,4 +1,4 @@ -import {CommandInteraction} from 'discord.js'; +import {ChatInputCommandInteraction} from 'discord.js'; import {SlashCommandBuilder} from '@discordjs/builders'; import {TYPES} from '../types.js'; import {inject, injectable} from 'inversify'; @@ -19,7 +19,7 @@ export default class implements Command { this.playerManager = playerManager; } - public async execute(interaction: CommandInteraction) { + public async execute(interaction: ChatInputCommandInteraction) { const player = this.playerManager.get(interaction.guild!.id); if (!player.voiceConnection) { diff --git a/src/commands/favorites.ts b/src/commands/favorites.ts index 31b1f79..3cfd716 100644 --- a/src/commands/favorites.ts +++ b/src/commands/favorites.ts @@ -1,5 +1,5 @@ import {SlashCommandBuilder} from '@discordjs/builders'; -import {AutocompleteInteraction, CommandInteraction, MessageEmbed} from 'discord.js'; +import {AutocompleteInteraction, ChatInputCommandInteraction, EmbedBuilder} from 'discord.js'; import {inject, injectable} from 'inversify'; import Command from '.'; import AddQueryToQueue from '../services/add-query-to-queue.js'; @@ -56,9 +56,9 @@ export default class implements Command { constructor(@inject(TYPES.Services.AddQueryToQueue) private readonly addQueryToQueue: AddQueryToQueue) {} - requiresVC = (interaction: CommandInteraction) => interaction.options.getSubcommand() === 'use'; + requiresVC = (interaction: ChatInputCommandInteraction) => interaction.options.getSubcommand() === 'use'; - async execute(interaction: CommandInteraction) { + async execute(interaction: ChatInputCommandInteraction) { switch (interaction.options.getSubcommand()) { case 'use': await this.use(interaction); @@ -100,7 +100,7 @@ export default class implements Command { }))); } - private async use(interaction: CommandInteraction) { + private async use(interaction: ChatInputCommandInteraction) { const name = interaction.options.getString('name')!.trim(); const favorite = await prisma.favoriteQuery.findFirst({ @@ -123,7 +123,7 @@ export default class implements Command { }); } - private async list(interaction: CommandInteraction) { + private async list(interaction: ChatInputCommandInteraction) { const favorites = await prisma.favoriteQuery.findMany({ where: { guildId: interaction.guild!.id, @@ -135,7 +135,7 @@ export default class implements Command { return; } - const embed = new MessageEmbed().setTitle('Favorites'); + const embed = new EmbedBuilder().setTitle('Favorites'); let description = ''; for (const favorite of favorites) { @@ -149,7 +149,7 @@ export default class implements Command { }); } - private async create(interaction: CommandInteraction) { + private async create(interaction: ChatInputCommandInteraction) { const name = interaction.options.getString('name')!.trim(); const query = interaction.options.getString('query')!.trim(); @@ -174,7 +174,7 @@ export default class implements Command { await interaction.reply('👍 favorite created'); } - private async remove(interaction: CommandInteraction) { + private async remove(interaction: ChatInputCommandInteraction) { const name = interaction.options.getString('name')!.trim(); const favorite = await prisma.favoriteQuery.findFirst({where: { diff --git a/src/commands/fseek.ts b/src/commands/fseek.ts index 7d0e553..c469948 100644 --- a/src/commands/fseek.ts +++ b/src/commands/fseek.ts @@ -1,4 +1,4 @@ -import {CommandInteraction} from 'discord.js'; +import {ChatInputCommandInteraction} from 'discord.js'; import {SlashCommandBuilder} from '@discordjs/builders'; import {TYPES} from '../types.js'; import {inject, injectable} from 'inversify'; @@ -25,7 +25,7 @@ export default class implements Command { this.playerManager = playerManager; } - public async execute(interaction: CommandInteraction): Promise<void> { + public async execute(interaction: ChatInputCommandInteraction): Promise<void> { const player = this.playerManager.get(interaction.guild!.id); const currentSong = player.getCurrent(); diff --git a/src/commands/index.ts b/src/commands/index.ts index 02349d2..60cf6ce 100644 --- a/src/commands/index.ts +++ b/src/commands/index.ts @@ -1,11 +1,11 @@ import {SlashCommandBuilder, SlashCommandSubcommandsOnlyBuilder} from '@discordjs/builders'; -import {AutocompleteInteraction, ButtonInteraction, CommandInteraction} from 'discord.js'; +import {AutocompleteInteraction, ButtonInteraction, ChatInputCommandInteraction} from 'discord.js'; export default interface Command { readonly slashCommand: Partial<SlashCommandBuilder | SlashCommandSubcommandsOnlyBuilder> & Pick<SlashCommandBuilder, 'toJSON'>; readonly handledButtonIds?: readonly string[]; - readonly requiresVC?: boolean | ((interaction: CommandInteraction) => boolean); - execute: (interaction: CommandInteraction) => Promise<void>; + readonly requiresVC?: boolean | ((interaction: ChatInputCommandInteraction) => boolean); + execute: (interaction: ChatInputCommandInteraction) => Promise<void>; handleButtonInteraction?: (interaction: ButtonInteraction) => Promise<void>; handleAutocompleteInteraction?: (interaction: AutocompleteInteraction) => Promise<void>; } diff --git a/src/commands/move.ts b/src/commands/move.ts index 9855c59..9063473 100644 --- a/src/commands/move.ts +++ b/src/commands/move.ts @@ -1,4 +1,4 @@ -import {CommandInteraction} from 'discord.js'; +import {ChatInputCommandInteraction} from 'discord.js'; import {inject, injectable} from 'inversify'; import {TYPES} from '../types.js'; import PlayerManager from '../managers/player.js'; @@ -26,7 +26,7 @@ export default class implements Command { this.playerManager = playerManager; } - public async execute(interaction: CommandInteraction): Promise<void> { + public async execute(interaction: ChatInputCommandInteraction): Promise<void> { const player = this.playerManager.get(interaction.guild!.id); const from = interaction.options.getInteger('from') ?? 1; diff --git a/src/commands/now-playing.ts b/src/commands/now-playing.ts index 999bb69..55060ed 100644 --- a/src/commands/now-playing.ts +++ b/src/commands/now-playing.ts @@ -1,4 +1,4 @@ -import {CommandInteraction} from 'discord.js'; +import {ChatInputCommandInteraction} from 'discord.js'; import {TYPES} from '../types.js'; import {inject, injectable} from 'inversify'; import PlayerManager from '../managers/player.js'; @@ -18,7 +18,7 @@ export default class implements Command { this.playerManager = playerManager; } - public async execute(interaction: CommandInteraction): Promise<void> { + public async execute(interaction: ChatInputCommandInteraction): Promise<void> { const player = this.playerManager.get(interaction.guild!.id); if (!player.getCurrent()) { diff --git a/src/commands/pause.ts b/src/commands/pause.ts index 7b9d295..b0381ae 100644 --- a/src/commands/pause.ts +++ b/src/commands/pause.ts @@ -1,4 +1,4 @@ -import {CommandInteraction} from 'discord.js'; +import {ChatInputCommandInteraction} from 'discord.js'; import {SlashCommandBuilder} from '@discordjs/builders'; import {TYPES} from '../types.js'; import {inject, injectable} from 'inversify'; @@ -20,7 +20,7 @@ export default class implements Command { this.playerManager = playerManager; } - public async execute(interaction: CommandInteraction) { + public async execute(interaction: ChatInputCommandInteraction) { const player = this.playerManager.get(interaction.guild!.id); if (player.status !== STATUS.PLAYING) { diff --git a/src/commands/play.ts b/src/commands/play.ts index ca23d61..d6149c4 100644 --- a/src/commands/play.ts +++ b/src/commands/play.ts @@ -1,4 +1,4 @@ -import {AutocompleteInteraction, CommandInteraction} from 'discord.js'; +import {AutocompleteInteraction, ChatInputCommandInteraction} from 'discord.js'; import {URL} from 'url'; import {SlashCommandBuilder} from '@discordjs/builders'; import {inject, injectable} from 'inversify'; @@ -43,8 +43,7 @@ export default class implements Command { this.addQueryToQueue = addQueryToQueue; } - // eslint-disable-next-line complexity - public async execute(interaction: CommandInteraction): Promise<void> { + public async execute(interaction: ChatInputCommandInteraction): Promise<void> { const query = interaction.options.getString('query')!; await this.addQueryToQueue.addToQueue({ diff --git a/src/commands/queue.ts b/src/commands/queue.ts index 4d75d42..9115ee6 100644 --- a/src/commands/queue.ts +++ b/src/commands/queue.ts @@ -1,4 +1,4 @@ -import {CommandInteraction} from 'discord.js'; +import {ChatInputCommandInteraction} from 'discord.js'; import {SlashCommandBuilder} from '@discordjs/builders'; import {inject, injectable} from 'inversify'; import {TYPES} from '../types.js'; @@ -22,7 +22,7 @@ export default class implements Command { this.playerManager = playerManager; } - public async execute(interaction: CommandInteraction) { + public async execute(interaction: ChatInputCommandInteraction) { const player = this.playerManager.get(interaction.guild!.id); const embed = buildQueueEmbed(player, interaction.options.getInteger('page') ?? 1); diff --git a/src/commands/remove.ts b/src/commands/remove.ts index 55c416f..5b470c6 100644 --- a/src/commands/remove.ts +++ b/src/commands/remove.ts @@ -1,4 +1,4 @@ -import {CommandInteraction} from 'discord.js'; +import {ChatInputCommandInteraction} from 'discord.js'; import {inject, injectable} from 'inversify'; import {TYPES} from '../types.js'; import PlayerManager from '../managers/player.js'; @@ -26,7 +26,7 @@ export default class implements Command { this.playerManager = playerManager; } - public async execute(interaction: CommandInteraction): Promise<void> { + public async execute(interaction: ChatInputCommandInteraction): Promise<void> { const player = this.playerManager.get(interaction.guild!.id); const position = interaction.options.getInteger('position') ?? 1; diff --git a/src/commands/resume.ts b/src/commands/resume.ts index eddc762..42758f2 100644 --- a/src/commands/resume.ts +++ b/src/commands/resume.ts @@ -6,7 +6,7 @@ import PlayerManager from '../managers/player.js'; import {STATUS} from '../services/player.js'; import {buildPlayingMessageEmbed} from '../utils/build-embed.js'; import {getMemberVoiceChannel, getMostPopularVoiceChannel} from '../utils/channels.js'; -import {CommandInteraction, GuildMember} from 'discord.js'; +import {ChatInputCommandInteraction, GuildMember} from 'discord.js'; @injectable() export default class implements Command { @@ -22,8 +22,7 @@ export default class implements Command { this.playerManager = playerManager; } - // eslint-disable-next-line complexity - public async execute(interaction: CommandInteraction): Promise<void> { + public async execute(interaction: ChatInputCommandInteraction): Promise<void> { const player = this.playerManager.get(interaction.guild!.id); const [targetVoiceChannel] = getMemberVoiceChannel(interaction.member as GuildMember) ?? getMostPopularVoiceChannel(interaction.guild!); if (player.status === STATUS.PLAYING) { diff --git a/src/commands/seek.ts b/src/commands/seek.ts index 00e9c2e..4be51a2 100644 --- a/src/commands/seek.ts +++ b/src/commands/seek.ts @@ -1,4 +1,4 @@ -import {CommandInteraction} from 'discord.js'; +import {ChatInputCommandInteraction} from 'discord.js'; import {TYPES} from '../types.js'; import {inject, injectable} from 'inversify'; import PlayerManager from '../managers/player.js'; @@ -26,7 +26,7 @@ export default class implements Command { this.playerManager = playerManager; } - public async execute(interaction: CommandInteraction): Promise<void> { + public async execute(interaction: ChatInputCommandInteraction): Promise<void> { const player = this.playerManager.get(interaction.guild!.id); const currentSong = player.getCurrent(); diff --git a/src/commands/shuffle.ts b/src/commands/shuffle.ts index 819c01c..a4a1497 100644 --- a/src/commands/shuffle.ts +++ b/src/commands/shuffle.ts @@ -1,4 +1,4 @@ -import {CommandInteraction} from 'discord.js'; +import {ChatInputCommandInteraction} from 'discord.js'; import {TYPES} from '../types.js'; import {inject, injectable} from 'inversify'; import PlayerManager from '../managers/player.js'; @@ -19,7 +19,7 @@ export default class implements Command { this.playerManager = playerManager; } - public async execute(interaction: CommandInteraction): Promise<void> { + public async execute(interaction: ChatInputCommandInteraction): Promise<void> { const player = this.playerManager.get(interaction.guild!.id); if (player.isQueueEmpty()) { diff --git a/src/commands/skip.ts b/src/commands/skip.ts index eb2746a..d8be5d8 100644 --- a/src/commands/skip.ts +++ b/src/commands/skip.ts @@ -1,4 +1,4 @@ -import {CommandInteraction} from 'discord.js'; +import {ChatInputCommandInteraction} from 'discord.js'; import {TYPES} from '../types.js'; import {inject, injectable} from 'inversify'; import PlayerManager from '../managers/player.js'; @@ -24,7 +24,7 @@ export default class implements Command { this.playerManager = playerManager; } - public async execute(interaction: CommandInteraction): Promise<void> { + public async execute(interaction: ChatInputCommandInteraction): Promise<void> { const numToSkip = interaction.options.getInteger('number') ?? 1; if (numToSkip < 1) { diff --git a/src/commands/stop.ts b/src/commands/stop.ts index 3c32c7f..71b33db 100644 --- a/src/commands/stop.ts +++ b/src/commands/stop.ts @@ -1,4 +1,4 @@ -import {CommandInteraction} from 'discord.js'; +import {ChatInputCommandInteraction} from 'discord.js'; import {SlashCommandBuilder} from '@discordjs/builders'; import {TYPES} from '../types.js'; import {inject, injectable} from 'inversify'; @@ -20,7 +20,7 @@ export default class implements Command { this.playerManager = playerManager; } - public async execute(interaction: CommandInteraction) { + public async execute(interaction: ChatInputCommandInteraction) { const player = this.playerManager.get(interaction.guild!.id); if (!player.voiceConnection) { diff --git a/src/commands/unskip.ts b/src/commands/unskip.ts index 936f4ef..31805d3 100644 --- a/src/commands/unskip.ts +++ b/src/commands/unskip.ts @@ -1,4 +1,4 @@ -import {CommandInteraction} from 'discord.js'; +import {ChatInputCommandInteraction} from 'discord.js'; import {TYPES} from '../types.js'; import {inject, injectable} from 'inversify'; import PlayerManager from '../managers/player.js'; @@ -20,7 +20,7 @@ export default class implements Command { this.playerManager = playerManager; } - public async execute(interaction: CommandInteraction): Promise<void> { + public async execute(interaction: ChatInputCommandInteraction): Promise<void> { const player = this.playerManager.get(interaction.guild!.id); try { diff --git a/src/events/guild-create.ts b/src/events/guild-create.ts index d71acda..f509e45 100644 --- a/src/events/guild-create.ts +++ b/src/events/guild-create.ts @@ -1,58 +1,44 @@ -import {Guild, Client} from 'discord.js'; +import {Client, Guild} from 'discord.js'; import container from '../inversify.config.js'; import Command from '../commands'; import {TYPES} from '../types.js'; import Config from '../services/config.js'; import {prisma} from '../utils/db.js'; import {REST} from '@discordjs/rest'; -import {Routes} from 'discord-api-types/v9'; -import updatePermissionsForGuild from '../utils/update-permissions-for-guild.js'; +import {Setting} from '@prisma/client'; +import registerCommandsOnGuild from '../utils/register-commands-on-guild.js'; -export default async (guild: Guild): Promise<void> => { - let invitedBy; - try { - const logs = await guild.fetchAuditLogs({type: 'BOT_ADD'}); - invitedBy = logs.entries.find(entry => entry.target?.id === guild.client.user?.id)?.executor; - } catch {} - - if (!invitedBy) { - console.warn(`Could not find user who invited Muse to ${guild.name} from the audit logs.`); - } - - await prisma.setting.upsert({ +export async function createGuildSettings(guild: Guild): Promise<Setting> { + return prisma.setting.upsert({ where: { guildId: guild.id, }, create: { guildId: guild.id, - invitedByUserId: invitedBy?.id, - }, - update: { - invitedByUserId: invitedBy?.id, }, + update: {}, }); +} + +export default async (guild: Guild): Promise<void> => { + await createGuildSettings(guild); const config = container.get<Config>(TYPES.Config); // Setup slash commands if (!config.REGISTER_COMMANDS_ON_BOT) { - const token = container.get<Config>(TYPES.Config).DISCORD_TOKEN; const client = container.get<Client>(TYPES.Client); - const rest = new REST({version: '9'}).setToken(token); + const rest = new REST({version: '10'}).setToken(config.DISCORD_TOKEN); - await rest.put( - Routes.applicationGuildCommands(client.user!.id, guild.id), - {body: container.getAll<Command>(TYPES.Command).map(command => command.slashCommand.toJSON())}, - ); + await registerCommandsOnGuild({ + rest, + applicationId: client.user!.id, + guildId: guild.id, + commands: container.getAll<Command>(TYPES.Command).map(command => command.slashCommand), + }); } - await updatePermissionsForGuild(guild); - - if (invitedBy) { - await invitedBy.send('👋 Hi! You just invited me to a server. I can\'t be used by your server members until you complete setup by running /config set-role in your server.'); - } else { - const owner = await guild.fetchOwner(); - await owner.send('👋 Hi! Someone (probably you) just invited me to a server you own. I can\'t be used by your server members until you complete setup by running /config set-role in your server.'); - } + const owner = await guild.fetchOwner(); + await owner.send('👋 Hi! Someone (probably you) just invited me to a server you own. I can\'t be used by your server members until you complete setup by running /config set-role in your server.'); }; diff --git a/src/events/guild-update.ts b/src/events/guild-update.ts deleted file mode 100644 index 837bbbe..0000000 --- a/src/events/guild-update.ts +++ /dev/null @@ -1,10 +0,0 @@ -import {Guild} from 'discord.js'; -import updatePermissionsForGuild from '../utils/update-permissions-for-guild.js'; - -const handleGuildUpdate = async (oldGuild: Guild, newGuild: Guild) => { - if (oldGuild.ownerId !== newGuild.ownerId) { - await updatePermissionsForGuild(newGuild); - } -}; - -export default handleGuildUpdate; diff --git a/src/inversify.config.ts b/src/inversify.config.ts index e4c30e3..f420071 100644 --- a/src/inversify.config.ts +++ b/src/inversify.config.ts @@ -2,7 +2,7 @@ import 'reflect-metadata'; import {Container} from 'inversify'; import {TYPES} from './types.js'; import Bot from './bot.js'; -import {Client, Intents} from 'discord.js'; +import {Client, GatewayIntentBits} from 'discord.js'; import ConfigProvider from './services/config.js'; // Managers @@ -41,12 +41,10 @@ import KeyValueCacheProvider from './services/key-value-cache.js'; const container = new Container(); // Intents -const intents = new Intents(); -intents.add(Intents.FLAGS.GUILDS); // To listen for guildCreate event -intents.add(Intents.FLAGS.GUILD_MESSAGES); // To listen for messages (messageCreate event) -intents.add(Intents.FLAGS.DIRECT_MESSAGE_REACTIONS); // To listen for message reactions (messageReactionAdd event) -intents.add(Intents.FLAGS.DIRECT_MESSAGES); // To receive the prefix message -intents.add(Intents.FLAGS.GUILD_VOICE_STATES); // To listen for voice state changes (voiceStateUpdate event) +const intents: GatewayIntentBits[] = []; +intents.push(GatewayIntentBits.Guilds); // To listen for guildCreate event +intents.push(GatewayIntentBits.GuildMessageReactions); // To listen for message reactions (messageReactionAdd event) +intents.push(GatewayIntentBits.GuildVoiceStates); // To listen for voice state changes (voiceStateUpdate event) // Bot container.bind<Bot>(TYPES.Bot).to(Bot).inSingletonScope(); diff --git a/src/services/add-query-to-queue.ts b/src/services/add-query-to-queue.ts index 7980ca0..15192b4 100644 --- a/src/services/add-query-to-queue.ts +++ b/src/services/add-query-to-queue.ts @@ -1,5 +1,5 @@ /* eslint-disable complexity */ -import {CommandInteraction, GuildMember} from 'discord.js'; +import {ChatInputCommandInteraction, GuildMember} from 'discord.js'; import {inject, injectable} from 'inversify'; import shuffle from 'array-shuffle'; import {TYPES} from '../types.js'; @@ -12,7 +12,8 @@ import {getMemberVoiceChannel, getMostPopularVoiceChannel} from '../utils/channe @injectable() export default class AddQueryToQueue { - constructor(@inject(TYPES.Services.GetSongs) private readonly getSongs: GetSongs, @inject(TYPES.Managers.Player) private readonly playerManager: PlayerManager) {} + constructor(@inject(TYPES.Services.GetSongs) private readonly getSongs: GetSongs, @inject(TYPES.Managers.Player) private readonly playerManager: PlayerManager) { + } public async addToQueue({ query, @@ -25,7 +26,7 @@ export default class AddQueryToQueue { addToFrontOfQueue: boolean; shuffleAdditions: boolean; shouldSplitChapters: boolean; - interaction: CommandInteraction; + interaction: ChatInputCommandInteraction; }): Promise<void> { const guildId = interaction.guild!.id; const player = this.playerManager.get(guildId); @@ -121,7 +122,11 @@ export default class AddQueryToQueue { } newSongs.forEach(song => { - player.add({...song, addedInChannelId: interaction.channel!.id, requestedBy: interaction.member!.user.id}, {immediate: addToFrontOfQueue ?? false}); + player.add({ + ...song, + addedInChannelId: interaction.channel!.id, + requestedBy: interaction.member!.user.id, + }, {immediate: addToFrontOfQueue ?? false}); }); const firstSong = newSongs[0]; diff --git a/src/services/config.ts b/src/services/config.ts index 1b4f4c9..ca4fc68 100644 --- a/src/services/config.ts +++ b/src/services/config.ts @@ -18,7 +18,7 @@ const CONFIG_MAP = { CACHE_DIR: path.join(DATA_DIR, 'cache'), CACHE_LIMIT_IN_BYTES: xbytes.parseSize(process.env.CACHE_LIMIT ?? '2GB'), BOT_STATUS: process.env.BOT_STATUS ?? 'online', - BOT_ACTIVITY_TYPE: process.env.BOT_ACTIVITY_TYPE ?? 'LISTENING', + BOT_ACTIVITY_TYPE: process.env.BOT_ACTIVITY_TYPE ?? 'Listening', BOT_ACTIVITY_URL: process.env.BOT_ACTIVITY_URL ?? '', BOT_ACTIVITY: process.env.BOT_ACTIVITY ?? 'music', } as const; diff --git a/src/services/player.ts b/src/services/player.ts index abcc8ca..c8a95a4 100644 --- a/src/services/player.ts +++ b/src/services/player.ts @@ -10,7 +10,7 @@ import { AudioPlayerState, AudioPlayerStatus, createAudioPlayer, - createAudioResource, + createAudioResource, DiscordGatewayAdapterCreator, joinVoiceChannel, StreamType, VoiceConnection, @@ -82,7 +82,7 @@ export default class { this.voiceConnection = joinVoiceChannel({ channelId: channel.id, guildId: channel.guild.id, - adapterCreator: channel.guild.voiceAdapterCreator, + adapterCreator: channel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator, }); } diff --git a/src/services/spotify-api.ts b/src/services/spotify-api.ts index 7f90aff..4347df2 100644 --- a/src/services/spotify-api.ts +++ b/src/services/spotify-api.ts @@ -47,7 +47,7 @@ export default class { items.push(...tracksResponse.items.map(playlistItem => playlistItem.track)); } - const tracks = this.limitTracks(items, playlistLimit).map(this.toSpotifyTrack); + const tracks = this.limitTracks(items.filter(i => i !== null) as SpotifyApi.TrackObjectSimplified[], playlistLimit).map(this.toSpotifyTrack); return [tracks, playlist]; } diff --git a/src/types.ts b/src/types.ts index fc1b5e2..3b64ea1 100644 --- a/src/types.ts +++ b/src/types.ts @@ -8,7 +8,6 @@ export const TYPES = { ThirdParty: Symbol('ThirdParty'), Managers: { Player: Symbol('PlayerManager'), - UpdatingQueueEmbed: Symbol('UpdatingQueueEmbed'), }, Services: { AddQueryToQueue: Symbol('AddQueryToQueue'), diff --git a/src/utils/build-embed.ts b/src/utils/build-embed.ts index c6e9551..3ee17ce 100644 --- a/src/utils/build-embed.ts +++ b/src/utils/build-embed.ts @@ -1,5 +1,5 @@ import getYouTubeID from 'get-youtube-id'; -import {MessageEmbed} from 'discord.js'; +import {EmbedBuilder} from 'discord.js'; import Player, {MediaSource, QueuedSong, STATUS} from '../services/player.js'; import getProgressBar from './get-progress-bar.js'; import {prettyTime} from './time.js'; @@ -50,7 +50,7 @@ const getPlayerUI = (player: Player) => { return `${button} ${progressBar} \`[${elapsedTime}]\` 🔉`; }; -export const buildPlayingMessageEmbed = (player: Player): MessageEmbed => { +export const buildPlayingMessageEmbed = (player: Player): EmbedBuilder => { const currentlyPlaying = player.getCurrent(); if (!currentlyPlaying) { @@ -58,10 +58,9 @@ export const buildPlayingMessageEmbed = (player: Player): MessageEmbed => { } const {artist, thumbnailUrl, requestedBy} = currentlyPlaying; - const message = new MessageEmbed(); - + const message = new EmbedBuilder(); message - .setColor(player.status === STATUS.PLAYING ? 'DARK_GREEN' : 'DARK_RED') + .setColor(player.status === STATUS.PLAYING ? 'DarkGreen' : 'DarkRed') .setTitle(player.status === STATUS.PLAYING ? 'Now Playing' : 'Paused') .setDescription(` **${getSongTitle(currentlyPlaying)}** @@ -77,7 +76,7 @@ export const buildPlayingMessageEmbed = (player: Player): MessageEmbed => { return message; }; -export const buildQueueEmbed = (player: Player, page: number): MessageEmbed => { +export const buildQueueEmbed = (player: Player, page: number): EmbedBuilder => { const currentlyPlaying = player.getCurrent(); if (!currentlyPlaying) { @@ -108,7 +107,7 @@ export const buildQueueEmbed = (player: Player, page: number): MessageEmbed => { const playlistTitle = playlist ? `(${playlist.title})` : ''; const totalLength = player.getQueue().reduce((accumulator, current) => accumulator + current.length, 0); - const message = new MessageEmbed(); + const message = new EmbedBuilder(); let description = `**${getSongTitle(currentlyPlaying)}**\n`; description += `Requested by: <@${requestedBy}>\n\n`; @@ -121,11 +120,11 @@ export const buildQueueEmbed = (player: Player, page: number): MessageEmbed => { message .setTitle(player.status === STATUS.PLAYING ? 'Now Playing' : 'Queued songs') - .setColor(player.status === STATUS.PLAYING ? 'DARK_GREEN' : 'NOT_QUITE_BLACK') + .setColor(player.status === STATUS.PLAYING ? 'DarkGreen' : 'NotQuiteBlack') .setDescription(description) - .addField('In queue', getQueueInfo(player), true) - .addField('Total length', `${totalLength > 0 ? prettyTime(totalLength) : '-'}`, true) - .addField('Page', `${page} out of ${maxQueuePage}`, true) + .addFields([{name: 'In queue', value: getQueueInfo(player), inline: true}, { + name: 'Total length', value: `${totalLength > 0 ? prettyTime(totalLength) : '-'}`, inline: true, + }, {name: 'Page', value: `${page} out of ${maxQueuePage}`, inline: true}]) .setFooter({text: `Source: ${artist} ${playlistTitle}`}); if (thumbnailUrl) { @@ -134,3 +133,4 @@ export const buildQueueEmbed = (player: Player, page: number): MessageEmbed => { return message; }; + diff --git a/src/utils/channels.ts b/src/utils/channels.ts index f4e576a..25f3eb8 100644 --- a/src/utils/channels.ts +++ b/src/utils/channels.ts @@ -1,9 +1,9 @@ -import {Guild, VoiceChannel, User, GuildMember} from 'discord.js'; +import {ChannelType, Guild, GuildMember, User, VoiceChannel} from 'discord.js'; export const isUserInVoice = (guild: Guild, user: User): boolean => { let inVoice = false; - guild.channels.cache.filter(channel => channel.type === 'GUILD_VOICE').forEach(channel => { + guild.channels.cache.filter(channel => channel.type === ChannelType.GuildVoice).forEach(channel => { if ((channel as VoiceChannel).members.find(member => member.id === user.id)) { inVoice = true; } @@ -22,7 +22,7 @@ export const getSizeWithoutBots = (channel: VoiceChannel): number => channel.mem export const getMemberVoiceChannel = (member?: GuildMember): [VoiceChannel, number] | null => { const channel = member?.voice?.channel; - if (channel && channel.type === 'GUILD_VOICE') { + if (channel && channel.type === ChannelType.GuildVoice) { return [ channel, getSizeWithoutBots(channel), @@ -41,7 +41,7 @@ export const getMostPopularVoiceChannel = (guild: Guild): [VoiceChannel, number] const voiceChannels: PopularResult[] = []; for (const [_, channel] of guild.channels.cache) { - if (channel.type === 'GUILD_VOICE') { + if (channel.type === ChannelType.GuildVoice) { const size = getSizeWithoutBots(channel); voiceChannels.push({ diff --git a/src/utils/get-youtube-and-spotify-suggestions-for.ts b/src/utils/get-youtube-and-spotify-suggestions-for.ts index 10f9394..6594b52 100644 --- a/src/utils/get-youtube-and-spotify-suggestions-for.ts +++ b/src/utils/get-youtube-and-spotify-suggestions-for.ts @@ -1,4 +1,4 @@ -import {ApplicationCommandOptionChoice} from 'discord.js'; +import {APIApplicationCommandOptionChoice} from 'discord-api-types/v10'; import SpotifyWebApi from 'spotify-web-api-node'; import getYouTubeSuggestionsFor from './get-youtube-suggestions-for.js'; @@ -14,7 +14,7 @@ const filterDuplicates = <T extends {name: string}>(items: T[]) => { return results; }; -const getYouTubeAndSpotifySuggestionsFor = async (query: string, spotify: SpotifyWebApi, limit = 10): Promise<ApplicationCommandOptionChoice[]> => { +const getYouTubeAndSpotifySuggestionsFor = async (query: string, spotify: SpotifyWebApi, limit = 10): Promise<APIApplicationCommandOptionChoice[]> => { const [youtubeSuggestions, spotifyResults] = await Promise.all([ getYouTubeSuggestionsFor(query), spotify.search(query, ['track', 'album'], {limit: 5}), @@ -35,7 +35,7 @@ const getYouTubeAndSpotifySuggestionsFor = async (query: string, spotify: Spotif const maxYouTubeSuggestions = limit - numOfSpotifySuggestions; const numOfYouTubeSuggestions = Math.min(maxYouTubeSuggestions, totalYouTubeResults); - const suggestions: ApplicationCommandOptionChoice[] = []; + const suggestions: APIApplicationCommandOptionChoice[] = []; suggestions.push( ...youtubeSuggestions diff --git a/src/utils/register-commands-on-guild.ts b/src/utils/register-commands-on-guild.ts new file mode 100644 index 0000000..ebd94e4 --- /dev/null +++ b/src/utils/register-commands-on-guild.ts @@ -0,0 +1,19 @@ +import {REST} from '@discordjs/rest'; +import {Routes} from 'discord-api-types/v10'; +import Command from '../commands'; + +interface RegisterCommandsOnGuildOptions { + rest: REST; + applicationId: string; + guildId: string; + commands: Array<Command['slashCommand']>; +} + +const registerCommandsOnGuild = async ({rest, applicationId, guildId, commands}: RegisterCommandsOnGuildOptions) => { + await rest.put( + Routes.applicationGuildCommands(applicationId, guildId), + {body: commands.map(command => command.toJSON())}, + ); +}; + +export default registerCommandsOnGuild; diff --git a/src/utils/update-permissions-for-guild.ts b/src/utils/update-permissions-for-guild.ts deleted file mode 100644 index 64110a7..0000000 --- a/src/utils/update-permissions-for-guild.ts +++ /dev/null @@ -1,53 +0,0 @@ -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, - }, - ]; - - if (settings.invitedByUserId) { - permissions.push({ - id: settings.invitedByUserId, - type: 'USER', - permission: true, - }); - } - - 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; |
