From d9518888a24a294a742b512b75c0f5af90335d36 Mon Sep 17 00:00:00 2001 From: Marcel Ebert Date: Wed, 10 May 2023 16:15:35 +0200 Subject: [PATCH] Refactor server files --- package.json | 2 +- src/bot/MyClient.ts | 23 +++- src/bot/commands/ban.ts | 4 +- src/bot/commands/music/MusicCommand.ts | 8 +- src/bot/commands/music/play.ts | 4 +- src/db/connection.ts | 5 +- src/libs/ActivityManager.ts | 4 +- src/libs/MusicPlayerManager.ts | 11 +- src/libs/StreamManager.ts | 145 +++++++++++++------------ src/libs/Youtube.ts | 2 +- src/libs/video-download.ts | 8 +- src/server/app.ts | 4 +- src/server/controllers/login.ts | 13 +-- src/utils/embeds.ts | 26 ++--- yarn.lock | 8 +- 15 files changed, 142 insertions(+), 125 deletions(-) diff --git a/package.json b/package.json index f96b59e..d87709d 100644 --- a/package.json +++ b/package.json @@ -45,7 +45,7 @@ "async": "^3.2.4", "bufferutil": "^4.0.7", "cors": "^2.8.5", - "discord-akairo": "^8.1.0", + "discord-akairo": "discord-akairo/discord-akairo", "discord-ytdl-core": "^5.0.4", "discord.js": "^14.11.0", "dotenv": "^16.0.3", diff --git a/src/bot/MyClient.ts b/src/bot/MyClient.ts index 2149116..5dfea82 100644 --- a/src/bot/MyClient.ts +++ b/src/bot/MyClient.ts @@ -1,5 +1,6 @@ import { AkairoClient, CommandHandler, InhibitorHandler, ListenerHandler } from "discord-akairo" import config from "../utils/config" +import { GatewayIntentBits } from "discord.js" export class MyClient extends AkairoClient { commandHandler: CommandHandler @@ -7,11 +8,23 @@ export class MyClient extends AkairoClient { listenerHandler: ListenerHandler constructor() { + const intents = [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.GuildMembers, + GatewayIntentBits.GuildVoiceStates, + GatewayIntentBits.GuildMessageReactions, + GatewayIntentBits.DirectMessages, + GatewayIntentBits.DirectMessageReactions, + GatewayIntentBits.MessageContent + ] super( { - ownerID: config.OWNER_ID + ownerID: config.OWNER_ID, }, - {} + { + intents, + } ) this.commandHandler = new CommandHandler(this, { @@ -20,13 +33,13 @@ export class MyClient extends AkairoClient { directory: __dirname + "/commands/", handleEdits: true, commandUtil: true, - prefix: "!" + prefix: "!", }) this.inhibitorHandler = new InhibitorHandler(this, { - directory: __dirname + "/inhibitors/" + directory: __dirname + "/inhibitors/", }) this.listenerHandler = new ListenerHandler(this, { - directory: __dirname + "/listeners/" + directory: __dirname + "/listeners/", }) this.commandHandler.loadAll() diff --git a/src/bot/commands/ban.ts b/src/bot/commands/ban.ts index d00dc3c..d3db9f7 100644 --- a/src/bot/commands/ban.ts +++ b/src/bot/commands/ban.ts @@ -11,8 +11,8 @@ class BanCommand extends Command { type: "member" } ], - clientPermissions: ["BAN_MEMBERS"], - userPermissions: ["BAN_MEMBERS"], + clientPermissions: ["BanMembers"], + userPermissions: ["BanMembers"], channel: "guild" }) } diff --git a/src/bot/commands/music/MusicCommand.ts b/src/bot/commands/music/MusicCommand.ts index d0ce50d..30cc8a6 100644 --- a/src/bot/commands/music/MusicCommand.ts +++ b/src/bot/commands/music/MusicCommand.ts @@ -1,6 +1,6 @@ import { Command } from "discord-akairo" import _ from "lodash" -import { Guild, GuildMember, Message, TextChannel, MessageEmbed } from "discord.js" +import { Guild, GuildMember, Message, TextChannel, Embed, EmbedBuilder } from "discord.js"; import MusicPlayer from "../../../libs/MusicPlayer" import MusicPlayerManager from "../../../libs/MusicPlayerManager" import { trackError } from "../../../utils/trackError" @@ -66,11 +66,11 @@ export abstract class MusicCommand extends Command { { leading: false, trailing: true } ) - sendMessageToChannel(message: string | MessageEmbed) { + sendMessageToChannel(message: string | EmbedBuilder) { const defaultChannel = this.message.channel - if (message instanceof MessageEmbed) { - defaultChannel.send(message) + if (message instanceof EmbedBuilder) { + defaultChannel.send({ embeds: [message]}) } else { messageQueue.push(message) this.sendStringMessage(defaultChannel) diff --git a/src/bot/commands/music/play.ts b/src/bot/commands/music/play.ts index f9b6779..180cb87 100644 --- a/src/bot/commands/music/play.ts +++ b/src/bot/commands/music/play.ts @@ -1,10 +1,10 @@ -import { MessageEmbed } from "discord.js" import Youtube from "../../../libs/Youtube" import Spotify from "../../../libs/Spotify" import { createEmbedForTrack, createEmbedForTracks, createEmbedsForSpotifyPlaylist } from "../../../utils/embeds" import MusicPlayer from "../../../libs/MusicPlayer" import { SpotifyHelper } from "../../../shared/utils/helpers" import { createAndSavePlaylistModel, createAndSaveTrackModel } from "../../../db/models/helper" +import { Embed, EmbedBuilder } from "discord.js"; async function getTrackFromYoutubeVideo(videoURL: string) { const track = await Youtube.createTrackFromURL(videoURL) @@ -32,7 +32,7 @@ async function handleSearch(searchTerm: string) { } export async function handlePlay(input: string, guildID: GuildID, musicPlayer: MusicPlayer) { - let reply: MessageEmbed | string = "" + let reply: EmbedBuilder | string = "" if (Youtube.describesYoutubePlaylist(input)) { const playlistID = new URL(input).searchParams.get("list") diff --git a/src/db/connection.ts b/src/db/connection.ts index 8b63a88..39911ff 100644 --- a/src/db/connection.ts +++ b/src/db/connection.ts @@ -2,10 +2,7 @@ import mongoose from "mongoose" export function connect(databasePath: string) { mongoose.connect(databasePath, { - useNewUrlParser: true, - useCreateIndex: true, - useUnifiedTopology: true, - useFindAndModify: false + autoIndex: true, }) const db = mongoose.connection diff --git a/src/libs/ActivityManager.ts b/src/libs/ActivityManager.ts index 7d6bde2..02d8926 100644 --- a/src/libs/ActivityManager.ts +++ b/src/libs/ActivityManager.ts @@ -13,13 +13,13 @@ class ActivityManager { setPlaying(content: string, url?: string) { if (this.user) { - this.user.setPresence({ activity: { type: "LISTENING", name: content, url }, afk: false, status: "online" }) + this.user.setPresence({ activities: [{ type: 2, name: content, url }], afk: false, status: "online" }) } } setIdle() { if (this.user) { - this.user.setPresence({}) + this.user.setPresence({ status: "idle" }) } } } diff --git a/src/libs/MusicPlayerManager.ts b/src/libs/MusicPlayerManager.ts index 388ce49..d83fcbb 100644 --- a/src/libs/MusicPlayerManager.ts +++ b/src/libs/MusicPlayerManager.ts @@ -1,19 +1,24 @@ -import { VoiceChannel } from "discord.js" +import { VoiceBasedChannel } from "discord.js" import { trackError } from "../utils/trackError" import MusicPlayer from "./MusicPlayer" import MusicPlayerObserver from "./MusicPlayerObserver" import StreamManager from "./StreamManager" +import { joinVoiceChannel } from "@discordjs/voice" export class MusicPlayerManager { private musicPlayerMap: { [key in GuildID]: MusicPlayer } = {} private musicPlayerObserverMap: { [key in GuildID]: MusicPlayerObserver } = {} - async createPlayerFor(guildID: GuildID, channel: VoiceChannel): Promise { + async createPlayerFor(guildID: GuildID, channel: VoiceBasedChannel): Promise { if (this.musicPlayerMap[guildID] !== undefined && !this.musicPlayerMap[guildID].destroyed) { throw new Error(`Player already exists for guild ${guildID}`) } - const connection = await channel.join() + const connection = joinVoiceChannel({ + channelId: channel.id, + guildId: channel.guild.id, + adapterCreator: channel.guild.voiceAdapterCreator, + }) const musicPlayer = new MusicPlayer(new StreamManager(connection)) this.musicPlayerMap[guildID] = musicPlayer diff --git a/src/libs/StreamManager.ts b/src/libs/StreamManager.ts index 71f901d..b6e79e8 100644 --- a/src/libs/StreamManager.ts +++ b/src/libs/StreamManager.ts @@ -1,14 +1,14 @@ -import { StreamDispatcher, VoiceConnection } from "discord.js" -import { get as httpGet, IncomingMessage } from "http" +import { get as httpGet } from "http" import { get as httpsGet } from "https" import { PartialObserver, Subject } from "rxjs" import { Readable } from "stream" import Youtube from "../libs/Youtube" import { trackError } from "../utils/trackError" +import { AudioPlayer, createAudioResource, StreamType, VoiceConnection } from "@discordjs/voice" class StreamManager { private voiceConnection: VoiceConnection - private musicDispatcher?: StreamDispatcher + private musicDispatcher?: AudioPlayer private streamSource?: Readable | Track private volume: number = 0.1 private subject: Subject @@ -24,7 +24,7 @@ class StreamManager { } this.volume = volume / 100 if (this.musicDispatcher) { - this.musicDispatcher.setVolume(this.volume) + // this.streamSource.setVolume(this.volume) } } @@ -33,7 +33,7 @@ class StreamManager { } get paused(): boolean { - return Boolean(this.musicDispatcher && this.musicDispatcher.paused) + return Boolean(this.musicDispatcher && this.musicDispatcher.state.status === "paused") } get playing(): boolean { @@ -43,7 +43,7 @@ class StreamManager { pause() { if (!this.musicDispatcher) { throw new Error("Can't pause before starting to play something.") - } else if (this.musicDispatcher.paused) { + } else if (this.musicDispatcher.state.status === "paused") { throw new Error("Stream paused already.") } else { this.musicDispatcher.pause(true) @@ -53,10 +53,10 @@ class StreamManager { resume() { if (!this.musicDispatcher) { throw new Error("Can't resume before starting to play something.") - } else if (!this.musicDispatcher.paused) { + } else if (!(this.musicDispatcher.state.status === "paused")) { throw new Error("Stream running already.") } else { - this.musicDispatcher.resume() + this.musicDispatcher.unpause() } } @@ -65,13 +65,17 @@ class StreamManager { const trackPaused = this.paused const resourceGetter = source.startsWith("https:") ? httpsGet : httpGet try { - resourceGetter(source, result => { + resourceGetter(source, (result) => { if (this.streamSource instanceof Readable) this.streamSource.unpipe() // unpipe because otherwise the stream will not work with the next voice connection - const soundDispatcher = this.voiceConnection.play(result, { highWaterMark: 512, volume: vol }) - soundDispatcher - .on("finish", () => { - if (this.streamSource) { - this.play(this.streamSource, trackPaused) + const audioResource = createAudioResource(result, { inlineVolume: true, inputType: StreamType.OggOpus }) + audioResource.volume?.setVolume(vol) + this.musicDispatcher.play(audioResource) + audioResource.audioPlayer + .on("stateChange", (oldState, newState) => { + if (oldState.status === "playing" && newState.status === "idle") { + if (this.streamSource) { + this.play(this.streamSource, trackPaused) + } } }) .on("error", (error: any) => { @@ -85,7 +89,7 @@ class StreamManager { async play(input: Track | Readable, trackPaused: boolean = false) { try { - let dispatcher: StreamDispatcher = null + let dispatcher: AudioPlayer = null if (input instanceof Readable) { dispatcher = await this.playReadable(input) } else { @@ -108,25 +112,23 @@ class StreamManager { const source = await Youtube.createReadableStreamFor(input) this.streamSource = source - const dispatcher = this.voiceConnection.play(source, { - volume: this.volume, - highWaterMark: 512, - type: "opus" - }) - - dispatcher - .on("debug", info => this.subject.next({ type: "debug", data: info })) - .on("start", () => this.subject.next({ type: "start" })) - .on("finish", () => { - // check if this is still the current/only one - if (this.musicDispatcher === dispatcher) { - this.streamSource = null - this.musicDispatcher = null - this.subject.next({ type: "finish" }) + const audioResource = createAudioResource(source, { inlineVolume: true, inputType: StreamType.OggOpus }) + audioResource.volume?.setVolume(this.volume) + this.musicDispatcher.play(audioResource) + + audioResource.audioPlayer + .on("debug", (info: any) => this.subject.next({ type: "debug", data: info })) + .on("stateChange", (oldState, newState) => { + if (oldState.status === "playing" && newState.status === "idle") { + if (this.musicDispatcher === audioResource.audioPlayer) { + this.streamSource = null + this.musicDispatcher = null + this.subject.next({ type: "finish" }) + } } }) .on("error", (error: any) => { - if (this.musicDispatcher === dispatcher) { + if (this.musicDispatcher === audioResource.audioPlayer) { if (source instanceof Readable) source.destroy() this.streamSource = null this.musicDispatcher = null @@ -134,67 +136,68 @@ class StreamManager { } trackError(error, "StreamManager.playYoutube error") }) - return dispatcher + return audioResource.audioPlayer } private async playUnknown(input: Track) { const resourceGetter = input.url.startsWith("https:") ? httpsGet : httpGet - const stream: Readable = await new Promise(resolve => { - resourceGetter(input.url, result => { + const stream: Readable = await new Promise((resolve) => { + resourceGetter(input.url, (result) => { resolve(result) }) }) this.streamSource = stream - const dispatcher = this.voiceConnection.play(stream, { - volume: this.volume - }) - - dispatcher - .on("debug", info => this.subject.next({ type: "debug", data: info })) - .on("start", () => this.subject.next({ type: "start" })) - .on("finish", () => { - // check if this is still the current/only one - if (this.musicDispatcher === dispatcher) { - this.streamSource = null - this.musicDispatcher = null - this.subject.next({ type: "finish" }) + const audioResource = createAudioResource(stream, { inlineVolume: true, inputType: StreamType.OggOpus }) + audioResource.volume?.setVolume(this.volume) + this.musicDispatcher.play(audioResource) + + audioResource.audioPlayer + .on("debug", (info: any) => this.subject.next({ type: "debug", data: info })) + .on("stateChange", (oldState, newState) => { + if (oldState.status === "playing" && newState.status === "idle") { + if (this.musicDispatcher === audioResource.audioPlayer) { + this.streamSource = null + this.musicDispatcher = null + this.subject.next({ type: "finish" }) + } + } else if (oldState.status === "playing") { + this.subject.next({ type: "start" }) } }) .on("error", (error: any) => { - if (this.musicDispatcher === dispatcher) { + if (this.musicDispatcher === audioResource.audioPlayer) { this.streamSource = null this.musicDispatcher = null this.subject.next({ type: "error", data: error && error.message ? error.message : error }) } trackError(error, "StreamManager.playUnknown error") }) - return dispatcher + return audioResource.audioPlayer } private async playReadable(input: Readable) { this.streamSource = input - const dispatcher = this.voiceConnection.play(input, { - volume: this.volume, - highWaterMark: 512, - type: "opus" - }) - - dispatcher - .on("debug", info => this.subject.next({ type: "debug", data: info })) - .on("start", () => this.subject.next({ type: "start" })) - .on("finish", () => { - // check if this is still the current/only one - if (this.musicDispatcher === dispatcher) { - input.destroy() - this.streamSource = null - this.musicDispatcher = null - this.subject.next({ type: "finish" }) + const audioResource = createAudioResource(input, { inlineVolume: true, inputType: StreamType.OggOpus }) + audioResource.volume?.setVolume(this.volume) + this.musicDispatcher.play(audioResource) + + this.musicDispatcher + .on("debug", (info: any) => this.subject.next({ type: "debug", data: info })) + .on("stateChange", (oldState, newState) => { + if (oldState.status === "playing" && newState.status === "idle") { + if (this.musicDispatcher === audioResource.audioPlayer) { + this.streamSource = null + this.musicDispatcher = null + this.subject.next({ type: "finish" }) + } + } else if (oldState.status === "playing") { + this.subject.next({ type: "start" }) } }) .on("error", (error: any) => { - if (this.musicDispatcher === dispatcher) { + if (this.musicDispatcher === audioResource.audioPlayer) { input.destroy() this.streamSource = null this.musicDispatcher = null @@ -203,7 +206,7 @@ class StreamManager { trackError(error, "StreamManager.playReadable error") }) - return dispatcher + return audioResource.audioPlayer } subscribe(subscriber: PartialObserver) { @@ -211,14 +214,12 @@ class StreamManager { } endCurrent() { - if (this.musicDispatcher && !this.musicDispatcher.writableFinished) { - this.musicDispatcher.end() - } + this.stop() } stop() { - if (this.musicDispatcher && !this.musicDispatcher.destroyed) { - this.musicDispatcher.destroy() + if (this.musicDispatcher && this.musicDispatcher.state.status !== "idle") { + this.musicDispatcher.stop() } } diff --git a/src/libs/Youtube.ts b/src/libs/Youtube.ts index 4b8221e..41faee3 100644 --- a/src/libs/Youtube.ts +++ b/src/libs/Youtube.ts @@ -82,7 +82,7 @@ export class Youtube { if (info.thumbnail_url) { return { medium: info.thumbnail_url } } else { - const thumbnails = info.player_response?.videoDetails?.thumbnail?.thumbnails + const thumbnails = info.player_response?.videoDetails?.thumbnails if (thumbnails) { return { small: thumbnails[0]?.url, diff --git a/src/libs/video-download.ts b/src/libs/video-download.ts index a3995d0..5996ecf 100644 --- a/src/libs/video-download.ts +++ b/src/libs/video-download.ts @@ -1,5 +1,5 @@ import ytdlDiscordWrapper from "discord-ytdl-core" -import Agent from "https-proxy-agent" +import { HttpsProxyAgent } from "https-proxy-agent"; import _ from "lodash" const proxies: string[] = [ @@ -19,12 +19,11 @@ function selectRandomProxy(blacklist: string[] = []) { export function downloadVideoWithProxy(url: string, seek?: number) { let attempts = 0 - let triedProxies: string[] = [] + const triedProxies: string[] = [] while (attempts < 3) { const proxyAddress = selectRandomProxy(triedProxies) try { - console.log("proxyAddress", proxyAddress) - const agent = Agent(proxyAddress) + const agent = new HttpsProxyAgent(proxyAddress) const stream = ytdlDiscordWrapper(url, { // encoderArgs: ["-af", "bass=g=5,dynaudnorm=f=200"], @@ -35,6 +34,7 @@ export function downloadVideoWithProxy(url: string, seek?: number) { }) return stream } catch (error) { + // tslint:disable-next-line:no-console console.error(error) triedProxies.push(proxyAddress) } finally { diff --git a/src/server/app.ts b/src/server/app.ts index 3a06832..b1b8dd1 100644 --- a/src/server/app.ts +++ b/src/server/app.ts @@ -18,7 +18,9 @@ export function initApp(client: MyClient) { const schema = createSchema(client) const apolloServer = new ApolloServer({ schema }) - apolloServer.applyMiddleware({ app }) + apolloServer.start().then(() => { + apolloServer.applyMiddleware({ app }) + }) const loginRouter = createLoginRouter(client) diff --git a/src/server/controllers/login.ts b/src/server/controllers/login.ts index 183ee13..25eebcb 100644 --- a/src/server/controllers/login.ts +++ b/src/server/controllers/login.ts @@ -11,7 +11,7 @@ interface Timeout { } // TODO change timeouts to use ip addresses -let loginTimeouts: Record = {} +const loginTimeouts: Record = {} function getTimeout(user: GuildMember) { return loginTimeouts[user.id] @@ -40,12 +40,12 @@ async function initLogin(user: GuildMember) { return response.content.toLowerCase().startsWith("y") || response.content.toLowerCase().startsWith("n") } - const collected = await dmChannel.awaitMessages(filter, { max: 1, time: 60000, errors: ["time"] }) + const collected = await dmChannel.awaitMessages({ filter, max: 1, time: 60000, errors: ["time"] }) const answer = collected.first().content if (answer.toLowerCase().startsWith("y")) { try { const token = jwt.sign({ guildID: user.guild.id, userID: user.id }, config.SECRET, { - expiresIn: "60d" + expiresIn: "60d", }) dmChannel.send("Login successful. You can head back to your browser.") return token @@ -72,16 +72,15 @@ export function createLoginRouter(client: MyClient) { loginRouter.post("/", async (request: LoginRequest, response) => { const body = request.body - const guild = client.guilds.cache.find(guild => guild.id === body.guildID) - const user = guild.members.cache.find(member => member.id === body.userID) + const guild = client.guilds.cache.find((guild) => guild.id === body.guildID) + const user = guild.members.cache.find((member) => member.id === body.userID) try { const token = await initLogin(user) - console.log("sending token", token) response.status(200).send({ token }) } catch (error) { return response.status(401).json({ - error: error.message + error: error.message, }) } }) diff --git a/src/utils/embeds.ts b/src/utils/embeds.ts index 57c36d6..234786a 100644 --- a/src/utils/embeds.ts +++ b/src/utils/embeds.ts @@ -1,9 +1,9 @@ -import { GuildMember, MessageEmbed } from "discord.js" +import { GuildMember, EmbedBuilder } from "discord.js"; import _ from "lodash" import { SpotifyHelper } from "../shared/utils/helpers" export function createEmbedForTrack(track: Track, requester?: GuildMember) { - const embed = new MessageEmbed() + const embed = new EmbedBuilder() .setColor("#0099ff") .setTitle(track.title) .setURL(track.url) @@ -15,7 +15,7 @@ export function createEmbedForTrack(track: Track, requester?: GuildMember) { } export function createEmbedForTracks(tracks: Track[], requester?: GuildMember) { - const embed = new MessageEmbed() + const embed = new EmbedBuilder() .setColor("#0099ff") .setTitle("Playlist") .setTimestamp() @@ -32,30 +32,30 @@ export function createEmbedForTracks(tracks: Track[], requester?: GuildMember) { const validStrings = splitIntoValidStrings(trackDescription) _.forEach(validStrings, (description, index) => { if (index === 0) { - embed.addField("Tracks", description) + embed.addFields({ name: "Tracks", value: description }) } else if (index <= 2) { - embed.addField("More...", description) + embed.addFields({ name: "More", value: description }) } else { - embed.addField("And even more", "...") + embed.addFields({ name: "And even more", value: "..." }) } }) if (requester) { - embed.setFooter(`Songs requested from ${requester.displayName}`) + embed.setFooter({text: `Songs requested from ${requester.displayName}`}) } return embed } export function createEmbedsForSpotifyPlaylist(playlist: Playlist, requester?: GuildMember) { - const playlistEmbed = new MessageEmbed() + const playlistEmbed = new EmbedBuilder() .setColor("#0099ff") .setTitle(playlist.name) - .setAuthor(playlist.owner) + .setAuthor({name: playlist.owner}) .setTimestamp() if (requester) { - playlistEmbed.setFooter(`Songs requested from ${requester.displayName}`) + playlistEmbed.setFooter({text: `Songs requested from ${requester.displayName}`}) } let tracklistDescription = "" @@ -68,11 +68,11 @@ export function createEmbedsForSpotifyPlaylist(playlist: Playlist, requester?: G _.forEach(descriptions, (description, index) => { if (index === 0) { - playlistEmbed.addField("Tracks", description) + playlistEmbed.addFields({ name: "Tracks", value: description }) } else if (index <= 2) { - playlistEmbed.addField("More...", description) + playlistEmbed.addFields({ name: "More", value: description }) } else { - playlistEmbed.addField("And even more", "...") + playlistEmbed.addFields({ name: "And even more", value: "..." }) } }) diff --git a/yarn.lock b/yarn.lock index 19cbd82..78bad6d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3212,7 +3212,7 @@ __metadata: bufferutil: "npm:^4.0.7" concurrently: "npm:^8.0.1" cors: "npm:^2.8.5" - discord-akairo: "npm:^8.1.0" + discord-akairo: discord-akairo/discord-akairo discord-ytdl-core: "npm:^5.0.4" discord.js: "npm:^14.11.0" dotenv: "npm:^16.0.3" @@ -3390,10 +3390,10 @@ __metadata: languageName: node linkType: hard -"discord-akairo@npm:^8.1.0": +discord-akairo@discord-akairo/discord-akairo: version: 8.1.0 - resolution: "discord-akairo@npm:8.1.0" - checksum: e66761715c10fa34b0442a4560f337f616e00dfa4af913b20a4658676eabc0b225fc1e91a84fb6cd26b565a4322b9cbe11dbe009f2bc774b6a431a6dd9e3ca08 + resolution: "discord-akairo@https://github.com/discord-akairo/discord-akairo.git#commit=905f69382957023601ebbb6f8a3a8b6b0f615bd1" + checksum: 471ed50dfc3f14e078e8f43181bbf3db5f1c468db2eb3f4c44773b723f2eaabaf950b67a4507602ed55043afc69d3f35500c14a9c79c395572debf19c056b2b1 languageName: node linkType: hard