diff --git a/package-lock.json b/package-lock.json index 39b4c1a..6882fde 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "dependencies": { "canvas": "^2.7.0", "chalk": "^4.1.0", - "discord.js": "^12.5.1", + "discord.js": "github:discordjs/discord.js", "discord.js-lavalink-lib": "^0.1.8", "figlet": "^1.5.0", "glob": "^7.1.6", @@ -20,7 +20,7 @@ "mathjs": "^9.3.0", "moment": "^2.29.1", "ms": "^2.1.3", - "onion-lasers": "^1.1.2", + "onion-lasers": "^1.2.0-unstable.0", "pet-pet-gif": "^1.0.8", "relevant-urban": "^2.0.0", "translate-google": "^1.4.3", @@ -41,7 +41,7 @@ "rimraf": "^3.0.2", "ts-jest": "^26.4.4", "tsc-watch": "^4.2.9", - "typescript": "^3.9.7" + "typescript": "^4.2.4" }, "optionalDependencies": { "fsevents": "^2.1.2" @@ -2348,21 +2348,20 @@ } }, "node_modules/discord.js": { - "version": "12.5.3", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.5.3.tgz", - "integrity": "sha512-D3nkOa/pCkNyn6jLZnAiJApw2N9XrIsXUAdThf01i7yrEuqUmDGc7/CexVWwEcgbQR97XQ+mcnqJpmJ/92B4Aw==", + "version": "12.5.0", + "resolved": "git+ssh://git@github.com/discordjs/discord.js.git#ab82cafcde0ee259a32ef14303c1b4a64dea8fae", + "license": "Apache-2.0", "dependencies": { "@discordjs/collection": "^0.1.6", "@discordjs/form-data": "^3.0.1", "abort-controller": "^3.0.0", "node-fetch": "^2.6.1", - "prism-media": "^1.2.9", - "setimmediate": "^1.0.5", + "prism-media": "^1.2.2", "tweetnacl": "^1.0.3", - "ws": "^7.4.4" + "ws": "^7.3.1" }, "engines": { - "node": ">=12.0.0" + "node": ">=14.0.0" } }, "node_modules/discord.js-lavalink-lib": { @@ -5406,11 +5405,11 @@ } }, "node_modules/onion-lasers": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/onion-lasers/-/onion-lasers-1.1.2.tgz", - "integrity": "sha512-gQHQCdcfDSLeWFFXMTBCy2PZR/n603B+Q2L3vTj+9T1CmJS7OfO7zoFM5QrTkOY4N5hESboOdJ8eRvPXQgdxDg==", + "version": "1.2.0-unstable.0", + "resolved": "https://registry.npmjs.org/onion-lasers/-/onion-lasers-1.2.0-unstable.0.tgz", + "integrity": "sha512-seKXo0CouLNNp2p/M0eORqQ56eJ6MNJe1dSXCI251we8OMJTI5+qRpzB1+OhR2/G4zRkNuWb2aXTUPYNsmVZrA==", "dependencies": { - "discord.js": "^12.5.3", + "discord.js": "github:discordjs/discord.js", "glob": "^7.1.6" } }, @@ -6506,11 +6505,6 @@ "node": ">=0.10.0" } }, - "node_modules/setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" - }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -7466,9 +7460,9 @@ } }, "node_modules/typescript": { - "version": "3.9.9", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", - "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", "dev": true, "bin": { "tsc": "bin/tsc", @@ -9800,18 +9794,16 @@ "dev": true }, "discord.js": { - "version": "12.5.3", - "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.5.3.tgz", - "integrity": "sha512-D3nkOa/pCkNyn6jLZnAiJApw2N9XrIsXUAdThf01i7yrEuqUmDGc7/CexVWwEcgbQR97XQ+mcnqJpmJ/92B4Aw==", + "version": "git+ssh://git@github.com/discordjs/discord.js.git#ab82cafcde0ee259a32ef14303c1b4a64dea8fae", + "from": "discord.js@github:discordjs/discord.js", "requires": { "@discordjs/collection": "^0.1.6", "@discordjs/form-data": "^3.0.1", "abort-controller": "^3.0.0", "node-fetch": "^2.6.1", - "prism-media": "^1.2.9", - "setimmediate": "^1.0.5", + "prism-media": "^1.2.2", "tweetnacl": "^1.0.3", - "ws": "^7.4.4" + "ws": "^7.3.1" } }, "discord.js-lavalink-lib": { @@ -12190,11 +12182,11 @@ } }, "onion-lasers": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/onion-lasers/-/onion-lasers-1.1.2.tgz", - "integrity": "sha512-gQHQCdcfDSLeWFFXMTBCy2PZR/n603B+Q2L3vTj+9T1CmJS7OfO7zoFM5QrTkOY4N5hESboOdJ8eRvPXQgdxDg==", + "version": "1.2.0-unstable.0", + "resolved": "https://registry.npmjs.org/onion-lasers/-/onion-lasers-1.2.0-unstable.0.tgz", + "integrity": "sha512-seKXo0CouLNNp2p/M0eORqQ56eJ6MNJe1dSXCI251we8OMJTI5+qRpzB1+OhR2/G4zRkNuWb2aXTUPYNsmVZrA==", "requires": { - "discord.js": "^12.5.3", + "discord.js": "github:discordjs/discord.js", "glob": "^7.1.6" } }, @@ -13018,11 +13010,6 @@ } } }, - "setimmediate": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", - "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" - }, "shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -13778,9 +13765,9 @@ } }, "typescript": { - "version": "3.9.9", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-3.9.9.tgz", - "integrity": "sha512-kdMjTiekY+z/ubJCATUPlRDl39vXYiMV9iyeMuEuXZh2we6zz80uovNN2WlAxmmdE/Z/YQe+EbOEXB5RHEED3w==", + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-4.2.4.tgz", + "integrity": "sha512-V+evlYHZnQkaz8TRBuxTA92yZBPotr5H+WhQ7bD3hZUndx5tGOa1fuCgeSjxAzM1RiN5IzvadIXTVefuuwZCRg==", "dev": true }, "underscore": { diff --git a/package.json b/package.json index 7347513..ead310f 100644 --- a/package.json +++ b/package.json @@ -12,12 +12,12 @@ "dev-instance": "rimraf dist && tsc && node . dev", "test": "jest", "format": "prettier --write **/*", - "postinstall": "husky install" + "postinstall": "node patch.js && husky install" }, "dependencies": { "canvas": "^2.7.0", "chalk": "^4.1.0", - "discord.js": "^12.5.1", + "discord.js": "github:discordjs/discord.js", "discord.js-lavalink-lib": "^0.1.8", "figlet": "^1.5.0", "glob": "^7.1.6", @@ -25,7 +25,7 @@ "mathjs": "^9.3.0", "moment": "^2.29.1", "ms": "^2.1.3", - "onion-lasers": "^1.1.2", + "onion-lasers": "^1.2.0-unstable.0", "pet-pet-gif": "^1.0.8", "relevant-urban": "^2.0.0", "translate-google": "^1.4.3", @@ -46,7 +46,7 @@ "rimraf": "^3.0.2", "ts-jest": "^26.4.4", "tsc-watch": "^4.2.9", - "typescript": "^3.9.7" + "typescript": "^4.2.4" }, "optionalDependencies": { "fsevents": "^2.1.2" diff --git a/patch.js b/patch.js new file mode 100644 index 0000000..3338d00 --- /dev/null +++ b/patch.js @@ -0,0 +1,23 @@ +// This is a nightmarishly bad way to handle module patches... but oh well, it's on the unstable branch for a reason. +const fs = require("fs"); +const DECLARATION_FILE = "node_modules/discord.js/typings/index.d.ts"; + +fs.readFile(DECLARATION_FILE, "utf-8", (err, data) => { + if (err) console.error(err); + else { + const declaration = data.split(/\r?\n/); + + // "discord-api-types/v8" is apparently not found so just ignore it to get the typings to work. + for (let i = 0; i < declaration.length; i++) { + const line = declaration[i]; + + if (line.includes("@ts-ignore")) { + break; + } else if (line.includes("discord-api-types/v8")) { + declaration.splice(i, 0, "// @ts-ignore"); + fs.writeFile(DECLARATION_FILE, declaration.join("\n"), () => {}); + break; + } + } + } +}); diff --git a/src/commands/system/admin.ts b/src/commands/system/admin.ts index 2e2d579..088d77e 100644 --- a/src/commands/system/admin.ts +++ b/src/commands/system/admin.ts @@ -282,7 +282,7 @@ export default new NamedCommand({ const newName = combined; if (!voiceChannel) return send("You are not in a voice channel."); - if (!guild!.me?.hasPermission(Permissions.FLAGS.MANAGE_CHANNELS)) + if (!guild!.me?.permissions.has(Permissions.FLAGS.MANAGE_CHANNELS)) return send("I can't change channel names without the `Manage Channels` permission."); guildStorage.channelNames[voiceChannel.id] = newName; @@ -332,6 +332,30 @@ export default new NamedCommand({ } }) }), + purge: new NamedCommand({ + description: "Purges the bot's own messages.", + permission: PERMISSIONS.BOT_SUPPORT, + channelType: CHANNEL_TYPE.GUILD, + async run({send, message, channel, guild, client}) { + // It's probably better to go through the bot's own messages instead of calling bulkDelete which requires MANAGE_MESSAGES. + if (guild!.me?.permissions.has(Permissions.FLAGS.MANAGE_MESSAGES)) { + message.delete(); + const msgs = await channel.messages.fetch({ + limit: 100 + }); + const travMessages = msgs.filter((m) => m.author.id === client.user?.id); + + await send(`Found ${travMessages.size} messages to delete.`).then((m) => { + setTimeout(() => { + m.delete(); + }, 5000); + }); + await (channel as TextChannel).bulkDelete(travMessages); + } else { + send("This command must be executed in a guild where I have the `MANAGE_MESSAGES` permission."); + } + } + }), clear: new NamedCommand({ description: "Clears a given amount of messages.", usage: "", diff --git a/src/commands/utility/desc.ts b/src/commands/utility/desc.ts index 68c5c17..beb8595 100644 --- a/src/commands/utility/desc.ts +++ b/src/commands/utility/desc.ts @@ -9,7 +9,7 @@ export default new NamedCommand({ const voiceChannel = message.member?.voice.channel; if (!voiceChannel) return send("You are not in a voice channel."); - if (!voiceChannel.guild.me?.hasPermission("MANAGE_CHANNELS")) + if (!voiceChannel.guild.me?.permissions.has("MANAGE_CHANNELS")) return send("I am lacking the required permissions to perform this action."); const prevName = voiceChannel.name; diff --git a/src/commands/utility/info.ts b/src/commands/utility/info.ts index e5b00f6..3811a0c 100644 --- a/src/commands/utility/info.ts +++ b/src/commands/utility/info.ts @@ -196,12 +196,13 @@ async function getGuildInfo(guild: Guild, currentGuild: Guild | null) { const iconURL = guild.iconURL({dynamic: true}); const embed = new MessageEmbed().setDescription(`**Guild information for __${guild.name}__**`).setColor("BLUE"); const displayRoles = !!(currentGuild && guild.id === currentGuild.id); + const owner = await guild.fetchOwner(); embed .addField("General", [ `**❯ Name:** ${guild.name}`, `**❯ ID:** ${guild.id}`, - `**❯ Owner:** ${guild.owner?.user.tag} (${guild.ownerID})`, + `**❯ Owner:** ${owner.user.tag} (${guild.ownerID})`, `**❯ Region:** ${regions[guild.region]}`, `**❯ Boost Tier:** ${guild.premiumTier ? `Tier ${guild.premiumTier}` : "None"}`, `**❯ Explicit Filter:** ${filterLevels[guild.explicitContentFilter]}`, diff --git a/src/commands/utility/lsemotes.ts b/src/commands/utility/lsemotes.ts index 921a573..22498c2 100644 --- a/src/commands/utility/lsemotes.ts +++ b/src/commands/utility/lsemotes.ts @@ -38,7 +38,9 @@ export default new NamedCommand({ let emotes = new Map(); for (const emote of emoteCollection) { - emotes.set(emote.id, emote.name); + if (emote.name) { + emotes.set(emote.id, emote.name); + } } // The result will be sandbox.emotes because it'll be modified in-place. @@ -77,6 +79,7 @@ export default new NamedCommand({ async function displayEmoteList(emotes: GuildEmoji[], send: SendFunction, author: User) { emotes.sort((a, b) => { + if (!a.name || !b.name) return 0; const first = a.name.toLowerCase(); const second = b.name.toLowerCase(); diff --git a/src/commands/utility/modules/emote-utils.ts b/src/commands/utility/modules/emote-utils.ts index 009dddc..43b2bd6 100644 --- a/src/commands/utility/modules/emote-utils.ts +++ b/src/commands/utility/modules/emote-utils.ts @@ -51,9 +51,11 @@ function searchSimilarEmotes(query: string): GuildEmoji[] { const emoteCandidates: {emote: GuildEmoji; dist: number}[] = []; for (const emote of client.emojis.cache.values()) { - const dist = levenshtein(emote.name, query); - if (dist <= maxAcceptedDistance) { - emoteCandidates.push({emote, dist}); + if (emote.name) { + const dist = levenshtein(emote.name, query); + if (dist <= maxAcceptedDistance) { + emoteCandidates.push({emote, dist}); + } } } diff --git a/src/commands/utility/say.ts b/src/commands/utility/say.ts index 60b5a52..775297f 100644 --- a/src/commands/utility/say.ts +++ b/src/commands/utility/say.ts @@ -47,7 +47,7 @@ export default new NamedCommand({ else send("Cannot send an empty message."); } - if (guild!.me?.hasPermission(Permissions.FLAGS.MANAGE_MESSAGES)) message.delete(); + if (guild!.me?.permissions.has(Permissions.FLAGS.MANAGE_MESSAGES)) message.delete(); } }) }); diff --git a/src/commands/utility/scanemotes.ts b/src/commands/utility/scanemotes.ts index 917f0bb..aed0d2a 100644 --- a/src/commands/utility/scanemotes.ts +++ b/src/commands/utility/scanemotes.ts @@ -24,7 +24,7 @@ export default new NamedCommand({ const stats: { [id: string]: { - name: string; + name: string | null; formatted: string; users: number; bots: number; diff --git a/src/index.ts b/src/index.ts index 7a85389..ee3144e 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,10 @@ import "./modules/globals"; -import {Client, Permissions} from "discord.js"; +import {Client, Permissions, Intents} from "discord.js"; import path from "path"; // This is here in order to make it much less of a headache to access the client from other files. // This of course won't actually do anything until the setup process is complete and it logs in. -export const client = new Client(); +export const client = new Client({intents: Intents.ALL}); import {launch} from "onion-lasers"; import setup from "./modules/setup"; @@ -31,15 +31,15 @@ launch(client, path.join(__dirname, "commands"), { name: "Moderator", check: (_user, member) => !!member && - (member.hasPermission(Permissions.FLAGS.MANAGE_ROLES) || - member.hasPermission(Permissions.FLAGS.MANAGE_MESSAGES) || - member.hasPermission(Permissions.FLAGS.KICK_MEMBERS) || - member.hasPermission(Permissions.FLAGS.BAN_MEMBERS)) + (member.permissions.has(Permissions.FLAGS.MANAGE_ROLES) || + member.permissions.has(Permissions.FLAGS.MANAGE_MESSAGES) || + member.permissions.has(Permissions.FLAGS.KICK_MEMBERS) || + member.permissions.has(Permissions.FLAGS.BAN_MEMBERS)) }, { // ADMIN // name: "Administrator", - check: (_user, member) => !!member && member.hasPermission(Permissions.FLAGS.ADMINISTRATOR) + check: (_user, member) => !!member && member.permissions.has(Permissions.FLAGS.ADMINISTRATOR) }, { // OWNER // diff --git a/src/modules/channelDefaults.ts b/src/modules/channelDefaults.ts index b346f2f..d43019b 100644 --- a/src/modules/channelDefaults.ts +++ b/src/modules/channelDefaults.ts @@ -10,7 +10,7 @@ client.on("voiceStateUpdate", async (before, after) => { channel && channel.members.size === 0 && channel.id in channelNames && - before.guild.me?.hasPermission(Permissions.FLAGS.MANAGE_CHANNELS) + before.guild.me?.permissions.has(Permissions.FLAGS.MANAGE_CHANNELS) ) { channel.setName(channelNames[channel.id]); } diff --git a/src/modules/eventLogging.ts b/src/modules/eventLogging.ts index 2e23bc6..1a7e1af 100644 --- a/src/modules/eventLogging.ts +++ b/src/modules/eventLogging.ts @@ -2,17 +2,49 @@ // Like with logging each command invocation, it's not a good idea to pollute the logs with this kind of stuff when it works most of the time. // However, it's also a pain to debug when no context is provided for an error message. import {client} from ".."; +import {setExecuteCommandListener} from "onion-lasers"; +import {TextChannel, DMChannel, NewsChannel} from "discord.js"; let lastEvent = "N/A"; +let lastCommandInfo: { + header: string; + args: string[]; + channel: TextChannel | DMChannel | NewsChannel | null; +} = { + header: "N/A", + args: [], + channel: null +}; -// A generic process handler is set to catch unhandled rejections other than the ones from Lavalink and Discord. process.on("unhandledRejection", (reason: any) => { const isLavalinkError = reason?.code === "ECONNREFUSED"; const isDiscordError = reason?.name === "DiscordAPIError"; - // If it's a DiscordAPIError on a message event, I'll make the assumption that it comes from the command handler. - if (!isLavalinkError && (!isDiscordError || lastEvent !== "message")) - console.error(`@${lastEvent}\n${reason.stack}`); + if (!isLavalinkError) { + // If it's a DiscordAPIError on a message event, I'll make the assumption that it comes from the command handler. + // That's not always the case though, especially if you add your own message events. Just be wary of that. + if (isDiscordError && lastEvent === "message") { + console.error( + `Command Error: ${lastCommandInfo.header} (${lastCommandInfo.args.join(", ")})\n${reason.stack}` + ); + lastCommandInfo.channel?.send( + `There was an error while trying to execute that command!\`\`\`${reason.stack}\`\`\`` + ); + } else { + console.error( + `@${lastEvent} : /${lastCommandInfo.header} (${lastCommandInfo.args.join(", ")})\n${reason.stack}` + ); + } + } +}); + +// Store info on which command was executed last. +setExecuteCommandListener(({header, args, channel}) => { + lastCommandInfo = { + header, + args, + channel + }; }); // This will dynamically attach all known events instead of doing it manually. diff --git a/src/modules/streamNotifications.ts b/src/modules/streamNotifications.ts index 0577b46..ab2f3ca 100644 --- a/src/modules/streamNotifications.ts +++ b/src/modules/streamNotifications.ts @@ -1,10 +1,10 @@ -import {GuildMember, VoiceChannel, MessageEmbed, TextChannel, Message, Collection} from "discord.js"; +import {GuildMember, VoiceChannel, MessageEmbed, TextChannel, Message, Collection, StageChannel} from "discord.js"; import {client} from "../index"; import {Storage} from "../structures"; type Stream = { streamer: GuildMember; - channel: VoiceChannel; + channel: VoiceChannel | StageChannel; category: string; description?: string; thumbnail?: string; @@ -19,7 +19,7 @@ export const streamList = new Collection(); // Probably find a better, DRY way of doing this. function getStreamEmbed( streamer: GuildMember, - channel: VoiceChannel, + channel: VoiceChannel | StageChannel, streamStart: number, category: string, description?: string, diff --git a/src/modules/systemInfo.ts b/src/modules/systemInfo.ts index afc64ca..439e517 100644 --- a/src/modules/systemInfo.ts +++ b/src/modules/systemInfo.ts @@ -5,21 +5,17 @@ import {Config} from "../structures"; // Logging which guilds the bot is added to and removed from makes sense. // However, logging the specific channels that are added/removed is a tad bit privacy-invading. -client.on("guildCreate", (guild) => { - console.log( - `[GUILD JOIN] ${guild.name} (${guild.id}) added the bot. Owner: ${guild.owner!.user.tag} (${ - guild.owner!.user.id - }).` - ); +client.on("guildCreate", async (guild) => { + const owner = await guild.fetchOwner(); + + console.log(`[GUILD JOIN] ${guild.name} (${guild.id}) added the bot. Owner: ${owner.user.tag} (${owner.user.id}).`); if (Config.systemLogsChannel) { const channel = client.channels.cache.get(Config.systemLogsChannel); if (channel && channel.type === "text") { (channel as TextChannel).send( - `TravBot joined: \`${guild.name}\`. The owner of this guild is: \`${guild.owner!.user.tag}\` (\`${ - guild.owner!.user.id - }\`)` + `TravBot joined: \`${guild.name}\`. The owner of this guild is: \`${owner.user.tag}\` (\`${owner.user.id}\`)` ); } else { console.warn(`${Config.systemLogsChannel} is not a valid text channel for system logs!`); diff --git a/src/modules/webhookStorageManager.ts b/src/modules/webhookStorageManager.ts index 245c958..9cb17ff 100644 --- a/src/modules/webhookStorageManager.ts +++ b/src/modules/webhookStorageManager.ts @@ -8,7 +8,7 @@ const ID_PATTERN = /(\d{17,})/; // Resolve any available webhooks available for a selected channel. export async function resolveWebhook(channel: TextChannel | NewsChannel): Promise { - if (channel.guild.me?.hasPermission(Permissions.FLAGS.MANAGE_WEBHOOKS)) { + if (channel.guild.me?.permissions.has(Permissions.FLAGS.MANAGE_WEBHOOKS)) { const webhooksInChannel = await channel.fetchWebhooks(); if (webhooksInChannel.size > 0) return webhooksInChannel.first()!; diff --git a/src/structures.ts b/src/structures.ts index 233f462..236e54f 100644 --- a/src/structures.ts +++ b/src/structures.ts @@ -243,9 +243,9 @@ export function getPrefix(guild: DiscordGuild | null): string { } export interface EmoteRegistryDumpEntry { - ref: string; + ref: string | null; id: Snowflake; - name: string; + name: string | null; requires_colons: boolean; animated: boolean; url: string;