diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/bot.ts | 7 | ||||
| -rw-r--r-- | src/commands/config.ts | 9 | ||||
| -rw-r--r-- | src/commands/help.ts | 12 | ||||
| -rw-r--r-- | src/commands/queue.ts | 2 | ||||
| -rw-r--r-- | src/commands/shortcuts.ts | 4 | ||||
| -rw-r--r-- | src/events/guild-create.ts | 6 | ||||
| -rw-r--r-- | src/events/voice-state-update.ts | 5 | ||||
| -rw-r--r-- | src/inversify.config.ts | 11 | ||||
| -rw-r--r-- | src/services/player.ts | 53 | ||||
| -rw-r--r-- | src/utils/channels.ts | 10 |
10 files changed, 73 insertions, 46 deletions
@@ -11,6 +11,7 @@ import handleVoiceStateUpdate from './events/voice-state-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'; @injectable() export default class { @@ -34,7 +35,7 @@ export default class { commandNames.forEach(commandName => this.commands.set(commandName, command)); }); - this.client.on('message', async (msg: Message) => { + this.client.on('messageCreate', async (msg: Message) => { // Get guild settings if (!msg.guild) { return; @@ -44,7 +45,8 @@ export default class { if (!settings) { // Got into a bad state, send owner welcome message - return this.client.emit('guildCreate', msg.guild); + this.client.emit('guildCreate', msg.guild); + return; } const {prefix, channel} = settings; @@ -95,6 +97,7 @@ export default class { }); this.client.on('ready', async () => { + debug(generateDependencyReport()); console.log(`Ready! Invite the bot with https://discordapp.com/oauth2/authorize?client_id=${this.client.user?.id ?? ''}&scope=bot&permissions=36752448`); }); diff --git a/src/commands/config.ts b/src/commands/config.ts index 0e5728a..8f3e0aa 100644 --- a/src/commands/config.ts +++ b/src/commands/config.ts @@ -1,4 +1,4 @@ -import {TextChannel, Message, GuildChannel} from 'discord.js'; +import {TextChannel, Message, GuildChannel, ThreadChannel} from 'discord.js'; import {injectable} from 'inversify'; import {Settings} from '../models/index.js'; import errorMsg from '../utils/error-msg.js'; @@ -20,6 +20,7 @@ export default class implements Command { if (settings) { let response = `prefix: \`${settings.prefix}\`\n`; + // eslint-disable-next-line @typescript-eslint/no-base-to-string response += `channel: ${msg.guild!.channels.cache.get(settings.channel)!.toString()}`; await msg.channel.send(response); @@ -35,7 +36,7 @@ export default class implements Command { return; } - if (msg.author.id !== msg.guild!.owner!.id) { + if (msg.author.id !== msg.guild!.ownerId) { await msg.channel.send(errorMsg('not authorized')); return; } @@ -51,7 +52,7 @@ export default class implements Command { } case 'channel': { - let channel: GuildChannel | undefined; + let channel: GuildChannel | ThreadChannel | undefined; if (args[1].includes('<#') && args[1].includes('>')) { channel = msg.guild!.channels.cache.find(c => c.id === args[1].slice(2, args[1].indexOf('>'))); @@ -59,7 +60,7 @@ export default class implements Command { channel = msg.guild!.channels.cache.find(c => c.name === args[1]); } - if (channel && channel.type === 'text') { + if (channel && channel.type === 'GUILD_TEXT') { await Settings.update({channel: channel.id}, {where: {guildId: msg.guild!.id}}); await Promise.all([ diff --git a/src/commands/help.ts b/src/commands/help.ts index f150c54..7058efd 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -1,4 +1,4 @@ -import {Message} from 'discord.js'; +import {Message, Util} from 'discord.js'; import {injectable} from 'inversify'; import Command from '.'; import {TYPES} from '../types.js'; @@ -29,7 +29,7 @@ export default class implements Command { const {prefix} = settings; - const res = this.commands.sort((a, b) => a.name.localeCompare(b.name)).reduce((content, command) => { + const res = Util.splitMessage(this.commands.sort((a, b) => a.name.localeCompare(b.name)).reduce((content, command) => { const aliases = command.aliases.reduce((str, alias, i) => { str += alias; @@ -53,9 +53,13 @@ export default class implements Command { content += '\n'; return content; - }, ''); + }, '')); + + for (const r of res) { + // eslint-disable-next-line no-await-in-loop + await msg.author.send(r); + } - await msg.author.send(res, {split: true}); await msg.react('🇩'); await msg.react('🇲'); } diff --git a/src/commands/queue.ts b/src/commands/queue.ts index 85420e8..1c0735c 100644 --- a/src/commands/queue.ts +++ b/src/commands/queue.ts @@ -74,7 +74,7 @@ export default class implements Command { embed.addField('Page', `${queuePage} out of ${maxQueuePage}`, false); - await msg.channel.send(embed); + await msg.channel.send({embeds: [embed]}); } else { await msg.channel.send('queue empty'); } diff --git a/src/commands/shortcuts.ts b/src/commands/shortcuts.ts index 109d5fc..929a5c1 100644 --- a/src/commands/shortcuts.ts +++ b/src/commands/shortcuts.ts @@ -55,7 +55,7 @@ export default class implements Command { const newShortcut = {shortcut: shortcutName, command, guildId: msg.guild!.id, authorId: msg.author.id}; if (shortcut) { - if (shortcut.authorId !== msg.author.id && msg.author.id !== msg.guild!.owner!.id) { + if (shortcut.authorId !== msg.author.id && msg.author.id !== msg.guild!.ownerId) { await msg.channel.send(errorMsg('you do\'nt have permission to do that')); return; } @@ -80,7 +80,7 @@ export default class implements Command { } // Check permissions - if (shortcut.authorId !== msg.author.id && msg.author.id !== msg.guild!.owner!.id) { + if (shortcut.authorId !== msg.author.id && msg.author.id !== msg.guild!.ownerId) { await msg.channel.send(errorMsg('you don\'t have permission to do that')); return; } diff --git a/src/events/guild-create.ts b/src/events/guild-create.ts index 3bcf281..01f0910 100644 --- a/src/events/guild-create.ts +++ b/src/events/guild-create.ts @@ -9,7 +9,7 @@ const DEFAULT_PREFIX = '!'; export default async (guild: Guild): Promise<void> => { await Settings.upsert({guildId: guild.id, prefix: DEFAULT_PREFIX}); - const owner = await guild.client.users.fetch(guild.ownerID); + const owner = await guild.client.users.fetch(guild.ownerId); let firstStep = '👋 Hi!\n'; firstStep += 'I just need to ask a few questions before you start listening to music.\n\n'; @@ -27,7 +27,7 @@ export default async (guild: Guild): Promise<void> => { const emojiChannels: EmojiChannel[] = []; for (const [channelId, channel] of guild.channels.cache) { - if (channel.type === 'text') { + if (channel.type === 'GUILD_TEXT') { emojiChannels.push({ name: channel.name, id: channelId, @@ -65,7 +65,7 @@ export default async (guild: Guild): Promise<void> => { await owner.send(secondStep); - const prefixResponses = await firstStepMsg.channel.awaitMessages((r: Message) => r.content.length === 1, {max: 1}); + const prefixResponses = await firstStepMsg.channel.awaitMessages({filter: (r: Message) => r.content.length === 1, max: 1}); const prefixCharacter = prefixResponses.first()!.content; diff --git a/src/events/voice-state-update.ts b/src/events/voice-state-update.ts index 6440949..69b4504 100644 --- a/src/events/voice-state-update.ts +++ b/src/events/voice-state-update.ts @@ -1,4 +1,4 @@ -import {VoiceState} from 'discord.js'; +import {VoiceChannel, VoiceState} from 'discord.js'; import container from '../inversify.config.js'; import {TYPES} from '../types.js'; import PlayerManager from '../managers/player.js'; @@ -10,7 +10,8 @@ export default (oldState: VoiceState, _: VoiceState): void => { const player = playerManager.get(oldState.guild.id); if (player.voiceConnection) { - if (getSizeWithoutBots(player.voiceConnection.channel) === 0) { + const voiceChannel: VoiceChannel = oldState.guild.channels.cache.get(player.voiceConnection.joinConfig.channelId!) as VoiceChannel; + if (!voiceChannel || getSizeWithoutBots(voiceChannel) === 0) { player.disconnect(); } } diff --git a/src/inversify.config.ts b/src/inversify.config.ts index 00a2507..c6809d2 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} from 'discord.js'; +import {Client, Intents} from 'discord.js'; import ConfigProvider from './services/config.js'; // Managers @@ -32,9 +32,16 @@ import CacheProvider from './services/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.GUILD_VOICE_STATES); // To listen for voice state changes (voiceStateUpdate event) + // Bot container.bind<Bot>(TYPES.Bot).to(Bot).inSingletonScope(); -container.bind<Client>(TYPES.Client).toConstantValue(new Client()); +container.bind<Client>(TYPES.Client).toConstantValue(new Client({intents})); // Managers container.bind<PlayerManager>(TYPES.Managers.Player).to(PlayerManager).inSingletonScope(); diff --git a/src/services/player.ts b/src/services/player.ts index 74f409b..9faef18 100644 --- a/src/services/player.ts +++ b/src/services/player.ts @@ -1,4 +1,4 @@ -import {VoiceConnection, VoiceChannel, StreamDispatcher, Snowflake, Client, TextChannel} from 'discord.js'; +import {VoiceChannel, Snowflake, Client, TextChannel} from 'discord.js'; import {promises as fs, createWriteStream} from 'fs'; import {Readable, PassThrough} from 'stream'; import path from 'path'; @@ -8,6 +8,7 @@ import {WriteStream} from 'fs-capacitor'; import ffmpeg from 'fluent-ffmpeg'; import shuffle from 'array-shuffle'; import errorMsg from '../utils/error-msg.js'; +import {AudioPlayer, AudioPlayerStatus, createAudioPlayer, createAudioResource, joinVoiceChannel, StreamType, VoiceConnection, VoiceConnectionStatus} from '@discordjs/voice'; export interface QueuedPlaylist { title: string; @@ -35,7 +36,7 @@ export default class { private queue: QueuedSong[] = []; private queuePosition = 0; private readonly cacheDir: string; - private dispatcher: StreamDispatcher | null = null; + private audioPlayer: AudioPlayer | null = null; private nowPlaying: QueuedSong | null = null; private playPositionInterval: NodeJS.Timeout | undefined; private lastSongURL = ''; @@ -50,23 +51,26 @@ export default class { } async connect(channel: VoiceChannel): Promise<void> { - const conn = await channel.join(); + const conn = joinVoiceChannel({ + channelId: channel.id, + guildId: channel.guild.id, + adapterCreator: channel.guild.voiceAdapterCreator, + }); this.voiceConnection = conn; } - disconnect(breakConnection = true): void { + disconnect(): void { if (this.voiceConnection) { if (this.status === STATUS.PLAYING) { this.pause(); } - if (breakConnection) { - this.voiceConnection.disconnect(); - } + this.voiceConnection.destroy(); + this.audioPlayer?.stop(); this.voiceConnection = null; - this.dispatcher = null; + this.audioPlayer = null; } } @@ -88,8 +92,11 @@ export default class { } const stream = await this.getStream(currentSong.url, {seek: positionSeconds}); - this.dispatcher = this.voiceConnection.play(stream, {type: 'webm/opus', bitrate: 'auto'}); - + this.audioPlayer = createAudioPlayer(); + this.voiceConnection.subscribe(this.audioPlayer); + this.audioPlayer.play(createAudioResource(stream, { + inputType: StreamType.WebmOpus, + })); this.attachListeners(); this.startTrackingPosition(positionSeconds); @@ -117,8 +124,8 @@ export default class { // Resume from paused state if (this.status === STATUS.PAUSED && currentSong.url === this.nowPlaying?.url) { - if (this.dispatcher) { - this.dispatcher.resume(); + if (this.audioPlayer) { + this.audioPlayer.unpause(); this.status = STATUS.PLAYING; this.startTrackingPosition(); return; @@ -132,7 +139,11 @@ export default class { try { const stream = await this.getStream(currentSong.url); - this.dispatcher = this.voiceConnection.play(stream, {type: 'webm/opus'}); + this.audioPlayer = createAudioPlayer(); + this.voiceConnection.subscribe(this.audioPlayer); + this.audioPlayer.play(createAudioResource(stream, { + inputType: StreamType.WebmOpus, + })); this.attachListeners(); @@ -170,8 +181,8 @@ export default class { this.status = STATUS.PAUSED; - if (this.dispatcher) { - this.dispatcher.pause(); + if (this.audioPlayer) { + this.audioPlayer.pause(); } this.stopTrackingPosition(); @@ -441,22 +452,22 @@ export default class { return; } - this.voiceConnection.on('disconnect', this.onVoiceConnectionDisconnect.bind(this)); + this.voiceConnection.on(VoiceConnectionStatus.Disconnected, this.onVoiceConnectionDisconnect.bind(this)); - if (!this.dispatcher) { + if (!this.audioPlayer) { return; } - this.dispatcher.on('speaking', this.onVoiceConnectionSpeaking.bind(this)); + this.audioPlayer.on('stateChange', this.onAudioPlayerStateChange.bind(this)); } private onVoiceConnectionDisconnect(): void { - this.disconnect(false); + this.disconnect(); } - private async onVoiceConnectionSpeaking(isSpeaking: boolean): Promise<void> { + private async onAudioPlayerStateChange(_oldState: {status: AudioPlayerStatus}, newState: {status: AudioPlayerStatus}): Promise<void> { // Automatically advance queued song at end - if (!isSpeaking && this.status === STATUS.PLAYING) { + if (newState.status === AudioPlayerStatus.Idle && this.status === STATUS.PLAYING) { await this.forward(1); } } diff --git a/src/utils/channels.ts b/src/utils/channels.ts index 4c5b6b0..2d074b4 100644 --- a/src/utils/channels.ts +++ b/src/utils/channels.ts @@ -3,8 +3,8 @@ import {Guild, VoiceChannel, User, GuildMember} from 'discord.js'; export const isUserInVoice = (guild: Guild, user: User): boolean => { let inVoice = false; - guild.channels.cache.filter(channel => channel.type === 'voice').forEach(channel => { - if (channel.members.array().find(member => member.id === user.id)) { + guild.channels.cache.filter(channel => channel.type === 'GUILD_VOICE').forEach(channel => { + if ((channel as VoiceChannel).members.find(member => member.id === user.id)) { inVoice = true; } }); @@ -12,7 +12,7 @@ export const isUserInVoice = (guild: Guild, user: User): boolean => { return inVoice; }; -export const getSizeWithoutBots = (channel: VoiceChannel): number => channel.members.array().reduce((s, member) => { +export const getSizeWithoutBots = (channel: VoiceChannel): number => channel.members.reduce((s, member) => { if (!member.user.bot) { s++; } @@ -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 === 'voice') { + if (channel && channel.type === 'GUILD_VOICE') { 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 === 'voice') { + if (channel.type === 'GUILD_VOICE') { const size = getSizeWithoutBots(channel as VoiceChannel); voiceChannels.push({ |
