fix command permission handling and push discord to v10 (#640)

Co-authored-by: Max Isom <hi@maxisom.me>
This commit is contained in:
Kevin Kendzia 2022-05-14 02:44:14 +02:00 committed by GitHub
parent 1ef05aba9d
commit eb2885b206
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
38 changed files with 1214 additions and 644 deletions

View file

@ -5,6 +5,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
### Changed
- Migrated to the v10 API
- Command permissions are now configured differently: you can now configure permissions in Discord's UI rather than through the bot. See the [wiki page](https://github.com/codetheweb/muse/wiki/Configuring-Bot-Permissions) for details.
- 🚨 when you upgrade to this version, the role you manually set with `/config set-role` will no longer be respected. Check the above link for how to re-configure permissions.
## [1.9.0] - 2022-04-23 ## [1.9.0] - 2022-04-23
### Changed ### Changed

View file

@ -0,0 +1,22 @@
/*
Warnings:
- You are about to drop the column `invitedByUserId` 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,
"secondsToWaitAfterQueueEmpties" INTEGER NOT NULL DEFAULT 30,
"leaveIfNoListeners" BOOLEAN NOT NULL DEFAULT true,
"roleId" TEXT,
"createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" DATETIME NOT NULL
);
INSERT INTO "new_Setting" ("createdAt", "guildId", "leaveIfNoListeners", "playlistLimit", "roleId", "secondsToWaitAfterQueueEmpties", "updatedAt") SELECT "createdAt", "guildId", "leaveIfNoListeners", "playlistLimit", "roleId", "secondsToWaitAfterQueueEmpties", "updatedAt" FROM "Setting";
DROP TABLE "Setting";
ALTER TABLE "new_Setting" RENAME TO "Setting";
PRAGMA foreign_key_check;
PRAGMA foreign_keys=ON;

View file

@ -49,7 +49,7 @@
"prisma": "^3.11.0", "prisma": "^3.11.0",
"release-it": "^14.11.8", "release-it": "^14.11.8",
"type-fest": "^2.12.0", "type-fest": "^2.12.0",
"typescript": "^4.6.2" "typescript": "^4.6.4"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [
@ -74,17 +74,17 @@
} }
}, },
"dependencies": { "dependencies": {
"@discordjs/builders": "^0.12.0", "@discordjs/builders": "0.14.0-dev.1652443433-d522320",
"@discordjs/opus": "^0.7.0", "@discordjs/opus": "^0.7.0",
"@discordjs/rest": "^0.3.0", "@discordjs/rest": "0.5.0-dev.1651147752-679dcda",
"@discordjs/voice": "^0.8.0", "@discordjs/voice": "0.10.0-dev.1651147759-679dcda",
"@prisma/client": "^3.11.0", "@prisma/client": "^3.11.0",
"@types/libsodium-wrappers": "^0.7.9", "@types/libsodium-wrappers": "^0.7.9",
"array-shuffle": "^3.0.0", "array-shuffle": "^3.0.0",
"debug": "^4.3.3", "debug": "^4.3.3",
"delay": "^5.0.0", "delay": "^5.0.0",
"discord-api-types": "^0.29.0", "discord-api-types": "0.32.1",
"discord.js": "^13.6.0", "discord.js": "14.0.0-dev.1652443445-d522320",
"dotenv": "^16.0.0", "dotenv": "^16.0.0",
"esmo": "0.14.1", "esmo": "0.14.1",
"execa": "^6.1.0", "execa": "^6.1.0",
@ -108,6 +108,7 @@
"reflect-metadata": "^0.1.13", "reflect-metadata": "^0.1.13",
"spotify-uri": "^2.2.0", "spotify-uri": "^2.2.0",
"spotify-web-api-node": "^5.0.2", "spotify-web-api-node": "^5.0.2",
"sync-fetch": "^0.3.1",
"xbytes": "^1.7.0", "xbytes": "^1.7.0",
"youtube.ts": "^0.2.8", "youtube.ts": "^0.2.8",
"ytdl-core": "^4.11.0", "ytdl-core": "^4.11.0",

View file

@ -25,7 +25,6 @@ model KeyValueCache {
model Setting { model Setting {
guildId String @id guildId String @id
invitedByUserId String?
playlistLimit Int @default(50) playlistLimit Int @default(50)
secondsToWaitAfterQueueEmpties Int @default(30) secondsToWaitAfterQueueEmpties Int @default(30)
leaveIfNoListeners Boolean @default(true) leaveIfNoListeners Boolean @default(true)

View file

@ -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 {inject, injectable} from 'inversify';
import ora from 'ora'; import ora from 'ora';
import {TYPES} from './types.js'; import {TYPES} from './types.js';
@ -7,32 +7,27 @@ import Command from './commands/index.js';
import debug from './utils/debug.js'; import debug from './utils/debug.js';
import handleGuildCreate from './events/guild-create.js'; import handleGuildCreate from './events/guild-create.js';
import handleVoiceStateUpdate from './events/voice-state-update.js'; import handleVoiceStateUpdate from './events/voice-state-update.js';
import handleGuildUpdate from './events/guild-update.js';
import errorMsg from './utils/error-msg.js'; import errorMsg from './utils/error-msg.js';
import {isUserInVoice} from './utils/channels.js'; import {isUserInVoice} from './utils/channels.js';
import Config from './services/config.js'; import Config from './services/config.js';
import {generateDependencyReport} from '@discordjs/voice'; import {generateDependencyReport} from '@discordjs/voice';
import {REST} from '@discordjs/rest'; import {REST} from '@discordjs/rest';
import {Routes} from 'discord-api-types/v9'; import {Routes, ActivityType} from 'discord-api-types/v10';
import updatePermissionsForGuild from './utils/update-permissions-for-guild.js'; import registerCommandsOnGuild from './utils/register-commands-on-guild.js';
import {ActivityTypes} from 'discord.js/typings/enums';
@injectable() @injectable()
export default class { export default class {
private readonly client: Client; private readonly client: Client;
private readonly config: Config; private readonly config: Config;
private readonly token: string;
private readonly shouldRegisterCommandsOnBot: boolean; private readonly shouldRegisterCommandsOnBot: boolean;
private readonly commandsByName!: Collection<string, Command>; private readonly commandsByName!: Collection<string, Command>;
private readonly commandsByButtonId!: Collection<string, Command>; private readonly commandsByButtonId!: Collection<string, Command>;
constructor( constructor(
@inject(TYPES.Client) client: Client, @inject(TYPES.Client) client: Client,
@inject(TYPES.Config) config: Config, @inject(TYPES.Config) config: Config) {
) {
this.client = client; this.client = client;
this.config = config; this.config = config;
this.token = config.DISCORD_TOKEN;
this.shouldRegisterCommandsOnBot = config.REGISTER_COMMANDS_ON_BOT; this.shouldRegisterCommandsOnBot = config.REGISTER_COMMANDS_ON_BOT;
this.commandsByName = new Collection(); this.commandsByName = new Collection();
this.commandsByButtonId = new Collection(); this.commandsByButtonId = new Collection();
@ -61,12 +56,13 @@ export default class {
} }
// Register event handlers // Register event handlers
// eslint-disable-next-line complexity
this.client.on('interactionCreate', async interaction => { this.client.on('interactionCreate', async interaction => {
try { try {
if (interaction.isCommand()) { if (interaction.isCommand()) {
const command = this.commandsByName.get(interaction.commandName); const command = this.commandsByName.get(interaction.commandName);
if (!command) { if (!command || !interaction.isChatInputCommand()) {
return; return;
} }
@ -76,7 +72,6 @@ export default class {
} }
const requiresVC = command.requiresVC instanceof Function ? command.requiresVC(interaction) : command.requiresVC; const requiresVC = command.requiresVC instanceof Function ? command.requiresVC(interaction) : command.requiresVC;
if (requiresVC && interaction.member && !isUserInVoice(interaction.guild, interaction.member.user as User)) { 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}); await interaction.reply({content: errorMsg('gotta be in a voice channel'), ephemeral: true});
return; 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 // This can fail if the message was deleted, and we don't want to crash the whole bot
try { 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)); 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}); await interaction.reply({content: errorMsg(error as Error), ephemeral: true});
} }
} catch {} } catch {}
@ -126,11 +121,9 @@ export default class {
debug(generateDependencyReport()); debug(generateDependencyReport());
// Update commands // 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) { if (this.shouldRegisterCommandsOnBot) {
spinner.text = '📡 updating commands on bot...'; spinner.text = '📡 updating commands on bot...';
await rest.put( await rest.put(
Routes.applicationCommands(this.client.user!.id), Routes.applicationCommands(this.client.user!.id),
{body: this.commandsByName.map(command => command.slashCommand.toJSON())}, {body: this.commandsByName.map(command => command.slashCommand.toJSON())},
@ -140,10 +133,12 @@ export default class {
await Promise.all([ await Promise.all([
...this.client.guilds.cache.map(async guild => { ...this.client.guilds.cache.map(async guild => {
await rest.put( await registerCommandsOnGuild({
Routes.applicationGuildCommands(this.client.user!.id, guild.id), rest,
{body: this.commandsByName.map(command => command.slashCommand.toJSON())}, guildId: guild.id,
); applicationId: this.client.user!.id,
commands: this.commandsByName.map(c => c.slashCommand),
});
}), }),
// Remove commands registered on bot (if they exist) // Remove commands registered on bot (if they exist)
rest.put(Routes.applicationCommands(this.client.user!.id), {body: []}), rest.put(Routes.applicationCommands(this.client.user!.id), {body: []}),
@ -155,18 +150,14 @@ export default class {
activities: [ activities: [
{ {
name: this.config.BOT_ACTIVITY, 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, url: this.config.BOT_ACTIVITY_URL === '' ? undefined : this.config.BOT_ACTIVITY_URL,
}, },
], ],
status: this.config.BOT_STATUS as PresenceStatusData, status: this.config.BOT_STATUS as PresenceStatusData,
}); });
// Update permissions spinner.succeed(`Ready! Invite the bot with https://discordapp.com/oauth2/authorize?client_id=${this.client.user?.id ?? ''}&scope=bot%20applications.commands&permissions=36700160`);
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`);
}); });
this.client.on('error', console.error); this.client.on('error', console.error);
@ -174,8 +165,6 @@ export default class {
this.client.on('guildCreate', handleGuildCreate); this.client.on('guildCreate', handleGuildCreate);
this.client.on('voiceStateUpdate', handleVoiceStateUpdate); this.client.on('voiceStateUpdate', handleVoiceStateUpdate);
this.client.on('guildUpdate', handleGuildUpdate); await this.client.login();
await this.client.login(this.token);
} }
} }

View file

@ -1,5 +1,5 @@
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
import {CommandInteraction} from 'discord.js'; import {ChatInputCommandInteraction} from 'discord.js';
import {SlashCommandBuilder} from '@discordjs/builders'; import {SlashCommandBuilder} from '@discordjs/builders';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import PlayerManager from '../managers/player.js'; import PlayerManager from '../managers/player.js';
@ -19,7 +19,7 @@ export default class implements Command {
this.playerManager = playerManager; this.playerManager = playerManager;
} }
public async execute(interaction: CommandInteraction) { public async execute(interaction: ChatInputCommandInteraction) {
this.playerManager.get(interaction.guild!.id).clear(); this.playerManager.get(interaction.guild!.id).clear();
await interaction.reply('clearer than a field after a fresh harvest'); await interaction.reply('clearer than a field after a fresh harvest');

View file

@ -1,8 +1,7 @@
import {SlashCommandBuilder} from '@discordjs/builders'; import {SlashCommandBuilder} from '@discordjs/builders';
import {CommandInteraction, MessageEmbed} from 'discord.js'; import {ChatInputCommandInteraction, EmbedBuilder, PermissionFlagsBits} from 'discord.js';
import {injectable} from 'inversify'; import {injectable} from 'inversify';
import {prisma} from '../utils/db.js'; import {prisma} from '../utils/db.js';
import updatePermissionsForGuild from '../utils/update-permissions-for-guild.js';
import Command from './index.js'; import Command from './index.js';
@injectable() @injectable()
@ -10,6 +9,7 @@ export default class implements Command {
public readonly slashCommand = new SlashCommandBuilder() public readonly slashCommand = new SlashCommandBuilder()
.setName('config') .setName('config')
.setDescription('configure bot settings') .setDescription('configure bot settings')
.setDefaultMemberPermissions(PermissionFlagsBits.ManageGuild.toString() as any)
.addSubcommand(subcommand => subcommand .addSubcommand(subcommand => subcommand
.setName('set-playlist-limit') .setName('set-playlist-limit')
.setDescription('set the maximum number of tracks that can be added from a playlist') .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') .setName('get')
.setDescription('show all settings')); .setDescription('show all settings'));
async execute(interaction: CommandInteraction) { async execute(interaction: ChatInputCommandInteraction) {
switch (interaction.options.getSubcommand()) { switch (interaction.options.getSubcommand()) {
case 'set-playlist-limit': { case 'set-playlist-limit': {
const limit = interaction.options.getInteger('limit')!; const limit: number = interaction.options.getInteger('limit')!;
if (limit < 1) { if (limit < 1) {
throw new Error('invalid limit'); throw new Error('invalid limit');
@ -66,25 +66,6 @@ export default class implements Command {
break; 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': { case 'set-wait-after-queue-empties': {
const delay = interaction.options.getInteger('delay')!; const delay = interaction.options.getInteger('delay')!;
@ -120,7 +101,7 @@ export default class implements Command {
} }
case 'get': { 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}}); const config = await prisma.setting.findUnique({where: {guildId: interaction.guild!.id}});

View file

@ -1,4 +1,4 @@
import {CommandInteraction} from 'discord.js'; import {ChatInputCommandInteraction} from 'discord.js';
import {SlashCommandBuilder} from '@discordjs/builders'; import {SlashCommandBuilder} from '@discordjs/builders';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
@ -19,7 +19,7 @@ export default class implements Command {
this.playerManager = playerManager; this.playerManager = playerManager;
} }
public async execute(interaction: CommandInteraction) { public async execute(interaction: ChatInputCommandInteraction) {
const player = this.playerManager.get(interaction.guild!.id); const player = this.playerManager.get(interaction.guild!.id);
if (!player.voiceConnection) { if (!player.voiceConnection) {

View file

@ -1,5 +1,5 @@
import {SlashCommandBuilder} from '@discordjs/builders'; 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 {inject, injectable} from 'inversify';
import Command from '.'; import Command from '.';
import AddQueryToQueue from '../services/add-query-to-queue.js'; 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) {} 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()) { switch (interaction.options.getSubcommand()) {
case 'use': case 'use':
await this.use(interaction); 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 name = interaction.options.getString('name')!.trim();
const favorite = await prisma.favoriteQuery.findFirst({ 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({ const favorites = await prisma.favoriteQuery.findMany({
where: { where: {
guildId: interaction.guild!.id, guildId: interaction.guild!.id,
@ -135,7 +135,7 @@ export default class implements Command {
return; return;
} }
const embed = new MessageEmbed().setTitle('Favorites'); const embed = new EmbedBuilder().setTitle('Favorites');
let description = ''; let description = '';
for (const favorite of favorites) { 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 name = interaction.options.getString('name')!.trim();
const query = interaction.options.getString('query')!.trim(); const query = interaction.options.getString('query')!.trim();
@ -174,7 +174,7 @@ export default class implements Command {
await interaction.reply('👍 favorite created'); await interaction.reply('👍 favorite created');
} }
private async remove(interaction: CommandInteraction) { private async remove(interaction: ChatInputCommandInteraction) {
const name = interaction.options.getString('name')!.trim(); const name = interaction.options.getString('name')!.trim();
const favorite = await prisma.favoriteQuery.findFirst({where: { const favorite = await prisma.favoriteQuery.findFirst({where: {

View file

@ -1,4 +1,4 @@
import {CommandInteraction} from 'discord.js'; import {ChatInputCommandInteraction} from 'discord.js';
import {SlashCommandBuilder} from '@discordjs/builders'; import {SlashCommandBuilder} from '@discordjs/builders';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
@ -25,7 +25,7 @@ export default class implements Command {
this.playerManager = playerManager; 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 player = this.playerManager.get(interaction.guild!.id);
const currentSong = player.getCurrent(); const currentSong = player.getCurrent();

View file

@ -1,11 +1,11 @@
import {SlashCommandBuilder, SlashCommandSubcommandsOnlyBuilder} from '@discordjs/builders'; import {SlashCommandBuilder, SlashCommandSubcommandsOnlyBuilder} from '@discordjs/builders';
import {AutocompleteInteraction, ButtonInteraction, CommandInteraction} from 'discord.js'; import {AutocompleteInteraction, ButtonInteraction, ChatInputCommandInteraction} from 'discord.js';
export default interface Command { export default interface Command {
readonly slashCommand: Partial<SlashCommandBuilder | SlashCommandSubcommandsOnlyBuilder> & Pick<SlashCommandBuilder, 'toJSON'>; readonly slashCommand: Partial<SlashCommandBuilder | SlashCommandSubcommandsOnlyBuilder> & Pick<SlashCommandBuilder, 'toJSON'>;
readonly handledButtonIds?: readonly string[]; readonly handledButtonIds?: readonly string[];
readonly requiresVC?: boolean | ((interaction: CommandInteraction) => boolean); readonly requiresVC?: boolean | ((interaction: ChatInputCommandInteraction) => boolean);
execute: (interaction: CommandInteraction) => Promise<void>; execute: (interaction: ChatInputCommandInteraction) => Promise<void>;
handleButtonInteraction?: (interaction: ButtonInteraction) => Promise<void>; handleButtonInteraction?: (interaction: ButtonInteraction) => Promise<void>;
handleAutocompleteInteraction?: (interaction: AutocompleteInteraction) => Promise<void>; handleAutocompleteInteraction?: (interaction: AutocompleteInteraction) => Promise<void>;
} }

View file

@ -1,4 +1,4 @@
import {CommandInteraction} from 'discord.js'; import {ChatInputCommandInteraction} from 'discord.js';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import PlayerManager from '../managers/player.js'; import PlayerManager from '../managers/player.js';
@ -26,7 +26,7 @@ export default class implements Command {
this.playerManager = playerManager; 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 player = this.playerManager.get(interaction.guild!.id);
const from = interaction.options.getInteger('from') ?? 1; const from = interaction.options.getInteger('from') ?? 1;

View file

@ -1,4 +1,4 @@
import {CommandInteraction} from 'discord.js'; import {ChatInputCommandInteraction} from 'discord.js';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player.js'; import PlayerManager from '../managers/player.js';
@ -18,7 +18,7 @@ export default class implements Command {
this.playerManager = playerManager; 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 player = this.playerManager.get(interaction.guild!.id);
if (!player.getCurrent()) { if (!player.getCurrent()) {

View file

@ -1,4 +1,4 @@
import {CommandInteraction} from 'discord.js'; import {ChatInputCommandInteraction} from 'discord.js';
import {SlashCommandBuilder} from '@discordjs/builders'; import {SlashCommandBuilder} from '@discordjs/builders';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
@ -20,7 +20,7 @@ export default class implements Command {
this.playerManager = playerManager; this.playerManager = playerManager;
} }
public async execute(interaction: CommandInteraction) { public async execute(interaction: ChatInputCommandInteraction) {
const player = this.playerManager.get(interaction.guild!.id); const player = this.playerManager.get(interaction.guild!.id);
if (player.status !== STATUS.PLAYING) { if (player.status !== STATUS.PLAYING) {

View file

@ -1,4 +1,4 @@
import {AutocompleteInteraction, CommandInteraction} from 'discord.js'; import {AutocompleteInteraction, ChatInputCommandInteraction} from 'discord.js';
import {URL} from 'url'; import {URL} from 'url';
import {SlashCommandBuilder} from '@discordjs/builders'; import {SlashCommandBuilder} from '@discordjs/builders';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
@ -43,8 +43,7 @@ export default class implements Command {
this.addQueryToQueue = addQueryToQueue; this.addQueryToQueue = addQueryToQueue;
} }
// eslint-disable-next-line complexity public async execute(interaction: ChatInputCommandInteraction): Promise<void> {
public async execute(interaction: CommandInteraction): Promise<void> {
const query = interaction.options.getString('query')!; const query = interaction.options.getString('query')!;
await this.addQueryToQueue.addToQueue({ await this.addQueryToQueue.addToQueue({

View file

@ -1,4 +1,4 @@
import {CommandInteraction} from 'discord.js'; import {ChatInputCommandInteraction} from 'discord.js';
import {SlashCommandBuilder} from '@discordjs/builders'; import {SlashCommandBuilder} from '@discordjs/builders';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
@ -22,7 +22,7 @@ export default class implements Command {
this.playerManager = playerManager; this.playerManager = playerManager;
} }
public async execute(interaction: CommandInteraction) { public async execute(interaction: ChatInputCommandInteraction) {
const player = this.playerManager.get(interaction.guild!.id); const player = this.playerManager.get(interaction.guild!.id);
const embed = buildQueueEmbed(player, interaction.options.getInteger('page') ?? 1); const embed = buildQueueEmbed(player, interaction.options.getInteger('page') ?? 1);

View file

@ -1,4 +1,4 @@
import {CommandInteraction} from 'discord.js'; import {ChatInputCommandInteraction} from 'discord.js';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import PlayerManager from '../managers/player.js'; import PlayerManager from '../managers/player.js';
@ -26,7 +26,7 @@ export default class implements Command {
this.playerManager = playerManager; 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 player = this.playerManager.get(interaction.guild!.id);
const position = interaction.options.getInteger('position') ?? 1; const position = interaction.options.getInteger('position') ?? 1;

View file

@ -6,7 +6,7 @@ import PlayerManager from '../managers/player.js';
import {STATUS} from '../services/player.js'; import {STATUS} from '../services/player.js';
import {buildPlayingMessageEmbed} from '../utils/build-embed.js'; import {buildPlayingMessageEmbed} from '../utils/build-embed.js';
import {getMemberVoiceChannel, getMostPopularVoiceChannel} from '../utils/channels.js'; import {getMemberVoiceChannel, getMostPopularVoiceChannel} from '../utils/channels.js';
import {CommandInteraction, GuildMember} from 'discord.js'; import {ChatInputCommandInteraction, GuildMember} from 'discord.js';
@injectable() @injectable()
export default class implements Command { export default class implements Command {
@ -22,8 +22,7 @@ export default class implements Command {
this.playerManager = playerManager; this.playerManager = playerManager;
} }
// eslint-disable-next-line complexity public async execute(interaction: ChatInputCommandInteraction): Promise<void> {
public async execute(interaction: CommandInteraction): Promise<void> {
const player = this.playerManager.get(interaction.guild!.id); const player = this.playerManager.get(interaction.guild!.id);
const [targetVoiceChannel] = getMemberVoiceChannel(interaction.member as GuildMember) ?? getMostPopularVoiceChannel(interaction.guild!); const [targetVoiceChannel] = getMemberVoiceChannel(interaction.member as GuildMember) ?? getMostPopularVoiceChannel(interaction.guild!);
if (player.status === STATUS.PLAYING) { if (player.status === STATUS.PLAYING) {

View file

@ -1,4 +1,4 @@
import {CommandInteraction} from 'discord.js'; import {ChatInputCommandInteraction} from 'discord.js';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player.js'; import PlayerManager from '../managers/player.js';
@ -26,7 +26,7 @@ export default class implements Command {
this.playerManager = playerManager; 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 player = this.playerManager.get(interaction.guild!.id);
const currentSong = player.getCurrent(); const currentSong = player.getCurrent();

View file

@ -1,4 +1,4 @@
import {CommandInteraction} from 'discord.js'; import {ChatInputCommandInteraction} from 'discord.js';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player.js'; import PlayerManager from '../managers/player.js';
@ -19,7 +19,7 @@ export default class implements Command {
this.playerManager = playerManager; 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 player = this.playerManager.get(interaction.guild!.id);
if (player.isQueueEmpty()) { if (player.isQueueEmpty()) {

View file

@ -1,4 +1,4 @@
import {CommandInteraction} from 'discord.js'; import {ChatInputCommandInteraction} from 'discord.js';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player.js'; import PlayerManager from '../managers/player.js';
@ -24,7 +24,7 @@ export default class implements Command {
this.playerManager = playerManager; this.playerManager = playerManager;
} }
public async execute(interaction: CommandInteraction): Promise<void> { public async execute(interaction: ChatInputCommandInteraction): Promise<void> {
const numToSkip = interaction.options.getInteger('number') ?? 1; const numToSkip = interaction.options.getInteger('number') ?? 1;
if (numToSkip < 1) { if (numToSkip < 1) {

View file

@ -1,4 +1,4 @@
import {CommandInteraction} from 'discord.js'; import {ChatInputCommandInteraction} from 'discord.js';
import {SlashCommandBuilder} from '@discordjs/builders'; import {SlashCommandBuilder} from '@discordjs/builders';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
@ -20,7 +20,7 @@ export default class implements Command {
this.playerManager = playerManager; this.playerManager = playerManager;
} }
public async execute(interaction: CommandInteraction) { public async execute(interaction: ChatInputCommandInteraction) {
const player = this.playerManager.get(interaction.guild!.id); const player = this.playerManager.get(interaction.guild!.id);
if (!player.voiceConnection) { if (!player.voiceConnection) {

View file

@ -1,4 +1,4 @@
import {CommandInteraction} from 'discord.js'; import {ChatInputCommandInteraction} from 'discord.js';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
import PlayerManager from '../managers/player.js'; import PlayerManager from '../managers/player.js';
@ -20,7 +20,7 @@ export default class implements Command {
this.playerManager = playerManager; 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 player = this.playerManager.get(interaction.guild!.id);
try { try {

View file

@ -1,58 +1,44 @@
import {Guild, Client} from 'discord.js'; import {Client, Guild} from 'discord.js';
import container from '../inversify.config.js'; import container from '../inversify.config.js';
import Command from '../commands'; import Command from '../commands';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
import Config from '../services/config.js'; import Config from '../services/config.js';
import {prisma} from '../utils/db.js'; import {prisma} from '../utils/db.js';
import {REST} from '@discordjs/rest'; import {REST} from '@discordjs/rest';
import {Routes} from 'discord-api-types/v9'; import {Setting} from '@prisma/client';
import updatePermissionsForGuild from '../utils/update-permissions-for-guild.js'; import registerCommandsOnGuild from '../utils/register-commands-on-guild.js';
export default async (guild: Guild): Promise<void> => { export async function createGuildSettings(guild: Guild): Promise<Setting> {
let invitedBy; return prisma.setting.upsert({
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({
where: { where: {
guildId: guild.id, guildId: guild.id,
}, },
create: { create: {
guildId: guild.id, 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); const config = container.get<Config>(TYPES.Config);
// Setup slash commands // Setup slash commands
if (!config.REGISTER_COMMANDS_ON_BOT) { if (!config.REGISTER_COMMANDS_ON_BOT) {
const token = container.get<Config>(TYPES.Config).DISCORD_TOKEN;
const client = container.get<Client>(TYPES.Client); 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( await registerCommandsOnGuild({
Routes.applicationGuildCommands(client.user!.id, guild.id), rest,
{body: container.getAll<Command>(TYPES.Command).map(command => command.slashCommand.toJSON())}, 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(); 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.'); 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.');
}
}; };

View file

@ -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;

View file

@ -2,7 +2,7 @@ import 'reflect-metadata';
import {Container} from 'inversify'; import {Container} from 'inversify';
import {TYPES} from './types.js'; import {TYPES} from './types.js';
import Bot from './bot.js'; import Bot from './bot.js';
import {Client, Intents} from 'discord.js'; import {Client, GatewayIntentBits} from 'discord.js';
import ConfigProvider from './services/config.js'; import ConfigProvider from './services/config.js';
// Managers // Managers
@ -41,12 +41,10 @@ import KeyValueCacheProvider from './services/key-value-cache.js';
const container = new Container(); const container = new Container();
// Intents // Intents
const intents = new Intents(); const intents: GatewayIntentBits[] = [];
intents.add(Intents.FLAGS.GUILDS); // To listen for guildCreate event intents.push(GatewayIntentBits.Guilds); // To listen for guildCreate event
intents.add(Intents.FLAGS.GUILD_MESSAGES); // To listen for messages (messageCreate event) intents.push(GatewayIntentBits.GuildMessageReactions); // To listen for message reactions (messageReactionAdd event)
intents.add(Intents.FLAGS.DIRECT_MESSAGE_REACTIONS); // To listen for message reactions (messageReactionAdd event) intents.push(GatewayIntentBits.GuildVoiceStates); // To listen for voice state changes (voiceStateUpdate 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)
// Bot // Bot
container.bind<Bot>(TYPES.Bot).to(Bot).inSingletonScope(); container.bind<Bot>(TYPES.Bot).to(Bot).inSingletonScope();

View file

@ -1,5 +1,5 @@
/* eslint-disable complexity */ /* eslint-disable complexity */
import {CommandInteraction, GuildMember} from 'discord.js'; import {ChatInputCommandInteraction, GuildMember} from 'discord.js';
import {inject, injectable} from 'inversify'; import {inject, injectable} from 'inversify';
import shuffle from 'array-shuffle'; import shuffle from 'array-shuffle';
import {TYPES} from '../types.js'; import {TYPES} from '../types.js';
@ -12,7 +12,8 @@ import {getMemberVoiceChannel, getMostPopularVoiceChannel} from '../utils/channe
@injectable() @injectable()
export default class AddQueryToQueue { 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({ public async addToQueue({
query, query,
@ -25,7 +26,7 @@ export default class AddQueryToQueue {
addToFrontOfQueue: boolean; addToFrontOfQueue: boolean;
shuffleAdditions: boolean; shuffleAdditions: boolean;
shouldSplitChapters: boolean; shouldSplitChapters: boolean;
interaction: CommandInteraction; interaction: ChatInputCommandInteraction;
}): Promise<void> { }): Promise<void> {
const guildId = interaction.guild!.id; const guildId = interaction.guild!.id;
const player = this.playerManager.get(guildId); const player = this.playerManager.get(guildId);
@ -121,7 +122,11 @@ export default class AddQueryToQueue {
} }
newSongs.forEach(song => { 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]; const firstSong = newSongs[0];

View file

@ -18,7 +18,7 @@ const CONFIG_MAP = {
CACHE_DIR: path.join(DATA_DIR, 'cache'), CACHE_DIR: path.join(DATA_DIR, 'cache'),
CACHE_LIMIT_IN_BYTES: xbytes.parseSize(process.env.CACHE_LIMIT ?? '2GB'), CACHE_LIMIT_IN_BYTES: xbytes.parseSize(process.env.CACHE_LIMIT ?? '2GB'),
BOT_STATUS: process.env.BOT_STATUS ?? 'online', 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_URL: process.env.BOT_ACTIVITY_URL ?? '',
BOT_ACTIVITY: process.env.BOT_ACTIVITY ?? 'music', BOT_ACTIVITY: process.env.BOT_ACTIVITY ?? 'music',
} as const; } as const;

View file

@ -10,7 +10,7 @@ import {
AudioPlayerState, AudioPlayerState,
AudioPlayerStatus, AudioPlayerStatus,
createAudioPlayer, createAudioPlayer,
createAudioResource, createAudioResource, DiscordGatewayAdapterCreator,
joinVoiceChannel, joinVoiceChannel,
StreamType, StreamType,
VoiceConnection, VoiceConnection,
@ -82,7 +82,7 @@ export default class {
this.voiceConnection = joinVoiceChannel({ this.voiceConnection = joinVoiceChannel({
channelId: channel.id, channelId: channel.id,
guildId: channel.guild.id, guildId: channel.guild.id,
adapterCreator: channel.guild.voiceAdapterCreator, adapterCreator: channel.guild.voiceAdapterCreator as DiscordGatewayAdapterCreator,
}); });
} }

View file

@ -47,7 +47,7 @@ export default class {
items.push(...tracksResponse.items.map(playlistItem => playlistItem.track)); 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]; return [tracks, playlist];
} }

View file

@ -8,7 +8,6 @@ export const TYPES = {
ThirdParty: Symbol('ThirdParty'), ThirdParty: Symbol('ThirdParty'),
Managers: { Managers: {
Player: Symbol('PlayerManager'), Player: Symbol('PlayerManager'),
UpdatingQueueEmbed: Symbol('UpdatingQueueEmbed'),
}, },
Services: { Services: {
AddQueryToQueue: Symbol('AddQueryToQueue'), AddQueryToQueue: Symbol('AddQueryToQueue'),

View file

@ -1,5 +1,5 @@
import getYouTubeID from 'get-youtube-id'; 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 Player, {MediaSource, QueuedSong, STATUS} from '../services/player.js';
import getProgressBar from './get-progress-bar.js'; import getProgressBar from './get-progress-bar.js';
import {prettyTime} from './time.js'; import {prettyTime} from './time.js';
@ -50,7 +50,7 @@ const getPlayerUI = (player: Player) => {
return `${button} ${progressBar} \`[${elapsedTime}]\` 🔉`; return `${button} ${progressBar} \`[${elapsedTime}]\` 🔉`;
}; };
export const buildPlayingMessageEmbed = (player: Player): MessageEmbed => { export const buildPlayingMessageEmbed = (player: Player): EmbedBuilder => {
const currentlyPlaying = player.getCurrent(); const currentlyPlaying = player.getCurrent();
if (!currentlyPlaying) { if (!currentlyPlaying) {
@ -58,10 +58,9 @@ export const buildPlayingMessageEmbed = (player: Player): MessageEmbed => {
} }
const {artist, thumbnailUrl, requestedBy} = currentlyPlaying; const {artist, thumbnailUrl, requestedBy} = currentlyPlaying;
const message = new MessageEmbed(); const message = new EmbedBuilder();
message 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') .setTitle(player.status === STATUS.PLAYING ? 'Now Playing' : 'Paused')
.setDescription(` .setDescription(`
**${getSongTitle(currentlyPlaying)}** **${getSongTitle(currentlyPlaying)}**
@ -77,7 +76,7 @@ export const buildPlayingMessageEmbed = (player: Player): MessageEmbed => {
return message; return message;
}; };
export const buildQueueEmbed = (player: Player, page: number): MessageEmbed => { export const buildQueueEmbed = (player: Player, page: number): EmbedBuilder => {
const currentlyPlaying = player.getCurrent(); const currentlyPlaying = player.getCurrent();
if (!currentlyPlaying) { if (!currentlyPlaying) {
@ -108,7 +107,7 @@ export const buildQueueEmbed = (player: Player, page: number): MessageEmbed => {
const playlistTitle = playlist ? `(${playlist.title})` : ''; const playlistTitle = playlist ? `(${playlist.title})` : '';
const totalLength = player.getQueue().reduce((accumulator, current) => accumulator + current.length, 0); const totalLength = player.getQueue().reduce((accumulator, current) => accumulator + current.length, 0);
const message = new MessageEmbed(); const message = new EmbedBuilder();
let description = `**${getSongTitle(currentlyPlaying)}**\n`; let description = `**${getSongTitle(currentlyPlaying)}**\n`;
description += `Requested by: <@${requestedBy}>\n\n`; description += `Requested by: <@${requestedBy}>\n\n`;
@ -121,11 +120,11 @@ export const buildQueueEmbed = (player: Player, page: number): MessageEmbed => {
message message
.setTitle(player.status === STATUS.PLAYING ? 'Now Playing' : 'Queued songs') .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) .setDescription(description)
.addField('In queue', getQueueInfo(player), true) .addFields([{name: 'In queue', value: getQueueInfo(player), inline: true}, {
.addField('Total length', `${totalLength > 0 ? prettyTime(totalLength) : '-'}`, true) name: 'Total length', value: `${totalLength > 0 ? prettyTime(totalLength) : '-'}`, inline: true,
.addField('Page', `${page} out of ${maxQueuePage}`, true) }, {name: 'Page', value: `${page} out of ${maxQueuePage}`, inline: true}])
.setFooter({text: `Source: ${artist} ${playlistTitle}`}); .setFooter({text: `Source: ${artist} ${playlistTitle}`});
if (thumbnailUrl) { if (thumbnailUrl) {
@ -134,3 +133,4 @@ export const buildQueueEmbed = (player: Player, page: number): MessageEmbed => {
return message; return message;
}; };

View file

@ -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 => { export const isUserInVoice = (guild: Guild, user: User): boolean => {
let inVoice = false; 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)) { if ((channel as VoiceChannel).members.find(member => member.id === user.id)) {
inVoice = true; inVoice = true;
} }
@ -22,7 +22,7 @@ export const getSizeWithoutBots = (channel: VoiceChannel): number => channel.mem
export const getMemberVoiceChannel = (member?: GuildMember): [VoiceChannel, number] | null => { export const getMemberVoiceChannel = (member?: GuildMember): [VoiceChannel, number] | null => {
const channel = member?.voice?.channel; const channel = member?.voice?.channel;
if (channel && channel.type === 'GUILD_VOICE') { if (channel && channel.type === ChannelType.GuildVoice) {
return [ return [
channel, channel,
getSizeWithoutBots(channel), getSizeWithoutBots(channel),
@ -41,7 +41,7 @@ export const getMostPopularVoiceChannel = (guild: Guild): [VoiceChannel, number]
const voiceChannels: PopularResult[] = []; const voiceChannels: PopularResult[] = [];
for (const [_, channel] of guild.channels.cache) { for (const [_, channel] of guild.channels.cache) {
if (channel.type === 'GUILD_VOICE') { if (channel.type === ChannelType.GuildVoice) {
const size = getSizeWithoutBots(channel); const size = getSizeWithoutBots(channel);
voiceChannels.push({ voiceChannels.push({

View file

@ -1,4 +1,4 @@
import {ApplicationCommandOptionChoice} from 'discord.js'; import {APIApplicationCommandOptionChoice} from 'discord-api-types/v10';
import SpotifyWebApi from 'spotify-web-api-node'; import SpotifyWebApi from 'spotify-web-api-node';
import getYouTubeSuggestionsFor from './get-youtube-suggestions-for.js'; import getYouTubeSuggestionsFor from './get-youtube-suggestions-for.js';
@ -14,7 +14,7 @@ const filterDuplicates = <T extends {name: string}>(items: T[]) => {
return results; 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([ const [youtubeSuggestions, spotifyResults] = await Promise.all([
getYouTubeSuggestionsFor(query), getYouTubeSuggestionsFor(query),
spotify.search(query, ['track', 'album'], {limit: 5}), spotify.search(query, ['track', 'album'], {limit: 5}),
@ -35,7 +35,7 @@ const getYouTubeAndSpotifySuggestionsFor = async (query: string, spotify: Spotif
const maxYouTubeSuggestions = limit - numOfSpotifySuggestions; const maxYouTubeSuggestions = limit - numOfSpotifySuggestions;
const numOfYouTubeSuggestions = Math.min(maxYouTubeSuggestions, totalYouTubeResults); const numOfYouTubeSuggestions = Math.min(maxYouTubeSuggestions, totalYouTubeResults);
const suggestions: ApplicationCommandOptionChoice[] = []; const suggestions: APIApplicationCommandOptionChoice[] = [];
suggestions.push( suggestions.push(
...youtubeSuggestions ...youtubeSuggestions

View file

@ -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;

View file

@ -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;

View file

@ -1,10 +1,9 @@
{ {
"compilerOptions": { "compilerOptions": {
"lib": ["ES2019", "DOM"], "lib": ["esnext"],
"target": "es2018", "target": "es2018",
"module": "ES2020", "module": "ES2020",
"moduleResolution": "node", "moduleResolution": "node",
"declaration": true,
"strict": true, "strict": true,
"experimentalDecorators": true, "experimentalDecorators": true,
"esModuleInterop": true, "esModuleInterop": true,

1451
yarn.lock

File diff suppressed because it is too large Load diff