From 2ba9e38f8f233dad9befe219feb4a832afe43dea Mon Sep 17 00:00:00 2001 From: Naomi Carrigan Date: Mon, 6 Jan 2025 17:43:53 -0800 Subject: [PATCH] feat: resolve linter issues --- src/commands/author.ts | 4 +- src/commands/community.ts | 2 +- src/commands/github.ts | 4 +- .../subcommands/community/handleForum.ts | 2 +- .../community/handleLeaderboard.ts | 157 +++++++++++------- .../subcommands/github/handleAddLabels.ts | 4 +- .../subcommands/github/handleClose.ts | 8 +- .../subcommands/github/handleComment.ts | 2 +- .../subcommands/github/handleSyncLabels.ts | 2 +- .../subcommands/moderation/handleBan.ts | 10 +- .../subcommands/moderation/handleKick.ts | 10 +- .../subcommands/moderation/handleMute.ts | 10 +- .../subcommands/moderation/handleUnmute.ts | 10 +- .../subcommands/moderation/handleWarn.ts | 10 +- src/config/pullComments.ts | 2 +- src/config/tags.ts | 2 +- src/contexts/snippet.ts | 94 ++++++----- src/events/handlers/handleMessageCreate.ts | 12 +- src/events/handlers/handleThreadCreate.ts | 2 +- src/gql.d.ts | 16 +- src/index.ts | 2 +- src/interfaces/forum.ts | 2 +- src/modules/automod/hasKnownPhishingLink.ts | 66 ++++---- src/modules/automod/hasNonApprovedInvite.ts | 61 +++---- src/modules/formatter.ts | 33 +++- src/modules/generateLogs.ts | 2 +- src/modules/generateProfileImage.ts | 10 +- src/modules/sendModerationDm.ts | 26 +-- src/server/serve.ts | 14 +- src/utils/fetchLearnRecord.ts | 2 +- src/utils/formatText.ts | 2 +- src/utils/loadCommands.ts | 33 ++-- src/utils/loadContexts.ts | 39 +++-- src/utils/loadQuotes.ts | 2 +- src/utils/registerCommands.ts | 5 +- .../community/handleCodeOfConduct.spec.ts | 8 +- test/utils/loadCommands.spec.ts | 2 +- 37 files changed, 397 insertions(+), 275 deletions(-) diff --git a/src/commands/author.ts b/src/commands/author.ts index 73a7d567..9e2b49ac 100644 --- a/src/commands/author.ts +++ b/src/commands/author.ts @@ -53,7 +53,6 @@ export const author: Command = { } const ourId = "65dc2b7cbb4eb0cd565b4463"; - // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment, @typescript-eslint/no-unsafe-call const query = gql` query getMember { user(username: "Koded001") { @@ -68,8 +67,7 @@ export const author: Command = { } `; const data - // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/consistent-type-assertions - = await request("https://gql.hashnode.com", query) as HashnodeUser; + = await request("https://gql.hashnode.com", query); if (!data.user) { await interaction.editReply({ diff --git a/src/commands/community.ts b/src/commands/community.ts index ba056f77..9ad21257 100644 --- a/src/commands/community.ts +++ b/src/commands/community.ts @@ -15,7 +15,7 @@ import type { Command } from "../interfaces/command.js"; import type { Subcommand } from "../interfaces/subcommand.js"; const handlers: Record = { - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention -- Discord command names can't use uppercase characters. "code-of-conduct": handleCodeOfConduct, "contribute": handleContribute, "forum": handleForum, diff --git a/src/commands/github.ts b/src/commands/github.ts index 7b94d259..901e5877 100644 --- a/src/commands/github.ts +++ b/src/commands/github.ts @@ -11,11 +11,11 @@ import type { Command } from "../interfaces/command.js"; import type { Subcommand } from "../interfaces/subcommand.js"; const handlers: Record = { - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention -- Discord command names can't use uppercase characters. "add-labels": handleAddLabels, "close": handleClose, "comment": handleComment, - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention -- Discord command names can't use uppercase characters. "sync-labels": handleSyncLabels, }; diff --git a/src/commands/subcommands/community/handleForum.ts b/src/commands/subcommands/community/handleForum.ts index 37f1a448..b3ade443 100644 --- a/src/commands/subcommands/community/handleForum.ts +++ b/src/commands/subcommands/community/handleForum.ts @@ -8,7 +8,7 @@ export const handleForum: Subcommand = { try { await interaction.deferReply(); const data = await fetch("https://forum.freecodecamp.org/latest.json"); - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- It kills me that .json() doesn't take a generic. const parsed = (await data.json()) as ForumData; const topics = parsed.topic_list.topics.slice(0, 5); const forumEmbed = new EmbedBuilder(). diff --git a/src/commands/subcommands/community/handleLeaderboard.ts b/src/commands/subcommands/community/handleLeaderboard.ts index 98fd7e25..e4aa7065 100644 --- a/src/commands/subcommands/community/handleLeaderboard.ts +++ b/src/commands/subcommands/community/handleLeaderboard.ts @@ -2,12 +2,93 @@ import { ActionRowBuilder, ButtonBuilder, ButtonStyle, + type ButtonInteraction, + type ChatInputCommandInteraction, type ComponentType, } from "discord.js"; import { generateLeaderboardImage } from "../../../modules/generateProfileImage.js"; import { errorHandler } from "../../../utils/errorHandler.js"; +import type { ExtendedClient } from "../../../interfaces/extendedClient.js"; import type { Subcommand } from "../../../interfaces/subcommand.js"; +import type { levels } from "@prisma/client"; + +const handleClickEnd = async( + pageBack: ButtonBuilder, + pageForward: ButtonBuilder, + interaction: ChatInputCommandInteraction, +): Promise => { + pageBack.setDisabled(true); + pageForward.setDisabled(true); + await interaction.editReply({ + components: [ + new ActionRowBuilder().addComponents( + pageBack, + pageForward, + ), + ], + }); +}; + +const handleClick = async( + camperChan: ExtendedClient, + parameters: { + click: ButtonInteraction; + interaction: ChatInputCommandInteraction; + pageBack: ButtonBuilder; + pageForward: ButtonBuilder; + pagination: { page: number; lastPage: number; itemsForPage: number }; + mapped: Array; + }, +): Promise => { + const { click, interaction, pageBack, pageForward, pagination, mapped } + = parameters; + let { page } = pagination; + const { lastPage, itemsForPage } = pagination; + await click.deferUpdate(); + if (click.customId === "prev") { + page = page - 1; + } + if (click.customId === "next") { + page = page + 1; + } + + if (page <= 1) { + pageBack.setDisabled(true); + } else { + pageBack.setDisabled(false); + } + + if (page >= lastPage) { + pageForward.setDisabled(true); + } else { + pageForward.setDisabled(false); + } + + const updatedAttachment = await generateLeaderboardImage( + camperChan, + mapped.slice(itemsForPage - 10, itemsForPage), + ); + + if (!updatedAttachment) { + await interaction.editReply({ + components: [], + content: "Failed to load leaderboard image.", + files: [], + }); + return; + } + + await interaction.editReply({ + components: [ + new ActionRowBuilder().addComponents( + pageBack, + pageForward, + ), + ], + files: [ updatedAttachment ], + }); +}; export const handleLeaderboard: Subcommand = { execute: async(camperChan, interaction) => { @@ -27,7 +108,7 @@ export const handleLeaderboard: Subcommand = { }; }); - let page = 1; + const page = 1; const lastPage = Math.ceil(mapped.length / 10); const pageBack = new ButtonBuilder(). @@ -39,13 +120,6 @@ export const handleLeaderboard: Subcommand = { setCustomId("next"). setLabel("▶"). setStyle(ButtonStyle.Primary); - - if (page <= 1) { - pageBack.setDisabled(true); - } else { - pageBack.setDisabled(false); - } - if (page >= lastPage) { pageForward.setDisabled(true); } else { @@ -87,65 +161,20 @@ export const handleLeaderboard: Subcommand = { time: 300_000, }); - // eslint-disable-next-line @typescript-eslint/no-misused-promises - clickyClick.on("collect", async(click) => { - await click.deferUpdate(); - if (click.customId === "prev") { - page = page - 1; - } - if (click.customId === "next") { - page = page + 1; - } - - if (page <= 1) { - pageBack.setDisabled(true); - } else { - pageBack.setDisabled(false); - } - - if (page >= lastPage) { - pageForward.setDisabled(true); - } else { - pageForward.setDisabled(false); - } - - const updatedAttachment = await generateLeaderboardImage( - camperChan, - mapped.slice(itemsForPage - 10, itemsForPage), - ); - - if (!updatedAttachment) { - await interaction.editReply({ - components: [], - content: "Failed to load leaderboard image.", - files: [], - }); - return; - } - - await interaction.editReply({ - components: [ - new ActionRowBuilder().addComponents( - pageBack, - pageForward, - ), - ], - files: [ updatedAttachment ], + clickyClick.on("collect", (click) => { + const pagination = { itemsForPage, lastPage, page }; + void handleClick(camperChan, { + click, + interaction, + mapped, + pageBack, + pageForward, + pagination, }); }); - // eslint-disable-next-line @typescript-eslint/no-misused-promises - clickyClick.on("end", async() => { - pageBack.setDisabled(true); - pageForward.setDisabled(true); - await interaction.editReply({ - components: [ - new ActionRowBuilder().addComponents( - pageBack, - pageForward, - ), - ], - }); + clickyClick.on("end", () => { + void handleClickEnd(pageBack, pageForward, interaction); }); } catch (error) { await errorHandler(camperChan, "leaderboard subcommand", error); diff --git a/src/commands/subcommands/github/handleAddLabels.ts b/src/commands/subcommands/github/handleAddLabels.ts index 06955a3a..55c60dd9 100644 --- a/src/commands/subcommands/github/handleAddLabels.ts +++ b/src/commands/subcommands/github/handleAddLabels.ts @@ -15,7 +15,7 @@ export const handleAddLabels: Subcommand = { const response = await camperChan.octokit.rest.issues.listLabelsForRepo({ owner: "freeCodeCamp", - // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase + // eslint-disable-next-line @typescript-eslint/naming-convention, camelcase -- Github API name. per_page: 100, repo: repo, }); @@ -44,7 +44,7 @@ export const handleAddLabels: Subcommand = { } await camperChan.octokit.rest.issues.addLabels({ - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention -- Github API name. issue_number: number, labels: labelNames, owner: "freeCodeCamp", diff --git a/src/commands/subcommands/github/handleClose.ts b/src/commands/subcommands/github/handleClose.ts index f841875e..6eb83243 100644 --- a/src/commands/subcommands/github/handleClose.ts +++ b/src/commands/subcommands/github/handleClose.ts @@ -28,7 +28,7 @@ export const handleClose: Subcommand = { const isSpam = interaction.options.getBoolean("spam") ?? false; const data = await camperChan.octokit.rest.issues.get({ - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention -- Github API name. issue_number: number, owner: "freeCodeCamp", repo: repo, @@ -47,13 +47,13 @@ export const handleClose: Subcommand = { const isPull = Boolean(data.data.pull_request); await camperChan.octokit.rest.issues.createComment({ body: commentBody(isPull, comment), - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention -- Github API name. issue_number: number, owner: "freeCodeCamp", repo: repo, }); await camperChan.octokit.rest.issues.update({ - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention -- Github API name. issue_number: number, owner: "freeCodeCamp", repo: repo, @@ -61,7 +61,7 @@ export const handleClose: Subcommand = { }); if (isPull && isSpam) { await camperChan.octokit.rest.issues.addLabels({ - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention -- Github API name. issue_number: number, labels: [ "spam" ], owner: "freeCodeCamp", diff --git a/src/commands/subcommands/github/handleComment.ts b/src/commands/subcommands/github/handleComment.ts index fdf63f26..064b4600 100644 --- a/src/commands/subcommands/github/handleComment.ts +++ b/src/commands/subcommands/github/handleComment.ts @@ -23,7 +23,7 @@ export const handleComment: Subcommand = { const comment = await camperChan.octokit.rest.issues.createComment({ body: message, - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention -- Github API name. issue_number: pull, owner: "freeCodeCamp", repo: repo, diff --git a/src/commands/subcommands/github/handleSyncLabels.ts b/src/commands/subcommands/github/handleSyncLabels.ts index 2c6d5d0e..567f33f6 100644 --- a/src/commands/subcommands/github/handleSyncLabels.ts +++ b/src/commands/subcommands/github/handleSyncLabels.ts @@ -14,7 +14,7 @@ export const handleSyncLabels: Subcommand = { }); await camperChan.octokit.rest.issues.setLabels({ - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention -- Github API name. issue_number: number, labels: labelNames, owner: "freeCodeCamp", diff --git a/src/commands/subcommands/moderation/handleBan.ts b/src/commands/subcommands/moderation/handleBan.ts index 2d504792..854a0ec5 100644 --- a/src/commands/subcommands/moderation/handleBan.ts +++ b/src/commands/subcommands/moderation/handleBan.ts @@ -40,10 +40,12 @@ export const handleBan: Subcommand = { const sentNotice = await sendModerationDm( camperChan, - "ban", - target, - guild.name, - reason, + { + action: "ban", + guildName: guild.name, + reason: customSubstring(reason, 1000), + user: target, + }, ); await targetMember.ban({ diff --git a/src/commands/subcommands/moderation/handleKick.ts b/src/commands/subcommands/moderation/handleKick.ts index 63ca17ff..75e54171 100644 --- a/src/commands/subcommands/moderation/handleKick.ts +++ b/src/commands/subcommands/moderation/handleKick.ts @@ -40,10 +40,12 @@ export const handleKick: Subcommand = { const sentNotice = await sendModerationDm( camperChan, - "kick", - target, - guild.name, - reason, + { + action: "kick", + guildName: guild.name, + reason: customSubstring(reason, 1000), + user: target, + }, ); await targetMember.kick(customSubstring(reason, 1000)); diff --git a/src/commands/subcommands/moderation/handleMute.ts b/src/commands/subcommands/moderation/handleMute.ts index 94906125..5021a2a1 100644 --- a/src/commands/subcommands/moderation/handleMute.ts +++ b/src/commands/subcommands/moderation/handleMute.ts @@ -67,10 +67,12 @@ export const handleMute: Subcommand = { const sentNotice = await sendModerationDm( camperChan, - "mute", - target, - guild.name, - reason, + { + action: "mute", + guildName: guild.name, + reason: customSubstring(reason, 1000), + user: target, + }, ); await targetMember.timeout(durationMilliseconds, reason); diff --git a/src/commands/subcommands/moderation/handleUnmute.ts b/src/commands/subcommands/moderation/handleUnmute.ts index 6a48c25d..046b4633 100644 --- a/src/commands/subcommands/moderation/handleUnmute.ts +++ b/src/commands/subcommands/moderation/handleUnmute.ts @@ -39,10 +39,12 @@ export const handleUnmute: Subcommand = { const sentNotice = await sendModerationDm( camperChan, - "unmute", - target, - guild.name, - reason, + { + action: "unmute", + guildName: guild.name, + reason: customSubstring(reason, 1000), + user: target, + }, ); const muteEmbed = new EmbedBuilder(); diff --git a/src/commands/subcommands/moderation/handleWarn.ts b/src/commands/subcommands/moderation/handleWarn.ts index e8177408..99ec3c5f 100644 --- a/src/commands/subcommands/moderation/handleWarn.ts +++ b/src/commands/subcommands/moderation/handleWarn.ts @@ -26,10 +26,12 @@ export const handleWarn: Subcommand = { const sentNotice = await sendModerationDm( camperChan, - "warn", - target, - guild.name, - reason, + { + action: "warn", + guildName: guild.name, + reason: customSubstring(reason, 1000), + user: target, + }, ); await updateHistory(camperChan, "warn", target.id); diff --git a/src/config/pullComments.ts b/src/config/pullComments.ts index 40d52a63..6870b145 100644 --- a/src/config/pullComments.ts +++ b/src/config/pullComments.ts @@ -1,4 +1,4 @@ -/* eslint-disable stylistic/max-len */ +/* eslint-disable stylistic/max-len -- These are config strings, so it's okay to not enforce length here. */ const pullComments: Array<{ key: string; message: string }> = [ { key: "Don't Ping Maintainers", diff --git a/src/config/tags.ts b/src/config/tags.ts index 87a58aaf..349c384b 100644 --- a/src/config/tags.ts +++ b/src/config/tags.ts @@ -1,4 +1,4 @@ -/* eslint-disable stylistic/max-len */ +/* eslint-disable stylistic/max-len -- These are config strings, so it's okay to not enforce length here. */ import type { Tag } from "../interfaces/tag.js"; export const tags: Array = [ diff --git a/src/contexts/snippet.ts b/src/contexts/snippet.ts index c99c3562..fe03a6a9 100644 --- a/src/contexts/snippet.ts +++ b/src/contexts/snippet.ts @@ -1,13 +1,59 @@ import { ActionRowBuilder, type ComponentType, + type ContextMenuCommandInteraction, + type Message, StringSelectMenuBuilder, + type StringSelectMenuInteraction, } from "discord.js"; import { tags } from "../config/tags.js"; import { errorHandler } from "../utils/errorHandler.js"; import { isModerator } from "../utils/isModerator.js"; import type { Context } from "../interfaces/context.js"; +const handleClickEnd = async( + interaction: ContextMenuCommandInteraction, +): Promise => { + await interaction. + editReply({ + components: [], + }). + + /** + * Ephemerals can be dismissed by the user. + * We catch the error here because we don't really + * care if it fails. + */ + catch(() => { + return null; + }); +}; + +const handleClick = async( + selection: StringSelectMenuInteraction, + interaction: ContextMenuCommandInteraction, + message: Message, +): Promise => { + await selection.deferUpdate(); + const name = selection.values.at(0); + const target = tags.find((t) => { + return t.name === name; + }); + if (!target) { + await selection.editReply({ + content: `Cannot find a snippet with the name ${String(name)}.`, + }); + return; + } + await message.reply({ + content: target.message, + }); + await interaction.editReply({ + components: [], + content: "Response sent!", + }); +}; + export const snippet: Context = { data: { name: "snippet", @@ -24,18 +70,13 @@ export const snippet: Context = { return; } await interaction.deferReply({ ephemeral: true }); - if ( - !isModerator(interaction.member) - ) { + if (!isModerator(interaction.member)) { await interaction.editReply({ content: "Only moderators may use this command.", }); return; } - const message = interaction.options.getMessage( - "message", - true, - ); + const message = interaction.options.getMessage("message", true); const dropdown = new StringSelectMenuBuilder(). setCustomId("snippets"). @@ -63,43 +104,12 @@ export const snippet: Context = { time: 1000 * 60 * 60, }); - // eslint-disable-next-line @typescript-eslint/no-misused-promises - collector.on("collect", async(selection) => { - await selection.deferUpdate(); - const name = selection.values.at(0); - const target = tags.find((t) => { - return t.name === name; - }); - if (!target) { - await selection.editReply({ - content: `Cannot find a snippet with the name ${String(name)}.`, - }); - return; - } - await message.reply({ - content: target.message, - }); - await interaction.editReply({ - components: [], - content: "Response sent!", - }); + collector.on("collect", (click) => { + void handleClick(click, interaction, message); }); - // eslint-disable-next-line @typescript-eslint/no-misused-promises - collector.on("end", async() => { - await interaction. - editReply({ - components: [], - }). - - /** - * Ephemerals can be dismissed by the user. - * We catch the error here because we don't really - * care if it fails. - */ - catch(() => { - return null; - }); + collector.on("end", () => { + void handleClickEnd(interaction); }); } catch (error) { await errorHandler(camperChan, "snippet context command", error); diff --git a/src/events/handlers/handleMessageCreate.ts b/src/events/handlers/handleMessageCreate.ts index e644c0f0..6bbac706 100644 --- a/src/events/handlers/handleMessageCreate.ts +++ b/src/events/handlers/handleMessageCreate.ts @@ -38,7 +38,7 @@ export const handleMessageCreate = async( if (message.content.startsWith("~cachebust")) { const [ , id ] = message.content.split(/\s+/g); if (id !== undefined) { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- Dynamic delete is required here. delete camperChan.learnAccounts[id]; } await message.reply(`Cache cleared for ${String(id)}`); @@ -114,10 +114,12 @@ export const handleMessageCreate = async( const reason = "Your account appears to be compromised."; const sentNotice = await sendModerationDm( camperChan, - "ban", - message.author, - message.guild.name, - reason, + { + action: "ban", + guildName: message.guild.name, + reason: customSubstring(reason, 1000), + user: message.author, + }, ); await message.member?.ban({ diff --git a/src/events/handlers/handleThreadCreate.ts b/src/events/handlers/handleThreadCreate.ts index ab8a74db..52cf2e16 100644 --- a/src/events/handlers/handleThreadCreate.ts +++ b/src/events/handlers/handleThreadCreate.ts @@ -12,7 +12,7 @@ export const handleThreadCreate = async( thread: ThreadChannel, ): Promise => { try { - // eslint-disable-next-line unicorn/require-array-join-separator + // eslint-disable-next-line unicorn/require-array-join-separator -- This is a Discord.js method, not an .join(). await thread.join(); const embed = new EmbedBuilder(); diff --git a/src/gql.d.ts b/src/gql.d.ts index 5d15c2c6..28ebac29 100644 --- a/src/gql.d.ts +++ b/src/gql.d.ts @@ -1 +1,15 @@ -declare module "graphql-request"; +declare module "graphql-request" { + type GraphQLQuery = + | `query ${string}` + | `mutation ${string}` + | `subscription ${string}`; + declare const gql: ( + template: TemplateStringsArray, + ...expressions: Array + )=> GraphQLQuery; + declare function request( + url: string, + query: GraphQLQuery, + variables?: Record + ): Promise; +} diff --git a/src/index.ts b/src/index.ts index ffd6238d..9d2324d7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -11,7 +11,7 @@ import { loadQuotes } from "./utils/loadQuotes.js"; import { registerCommands } from "./utils/registerCommands.js"; import type { ExtendedClient } from "./interfaces/extendedClient.js"; -// eslint-disable-next-line @typescript-eslint/consistent-type-assertions +// eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- The assertion here allows us to extend the Client interface. const camperChan = new Client({ intents: intentOptions, }) as ExtendedClient; diff --git a/src/interfaces/forum.ts b/src/interfaces/forum.ts index 295b6751..62360263 100644 --- a/src/interfaces/forum.ts +++ b/src/interfaces/forum.ts @@ -1,4 +1,4 @@ -/* eslint-disable @typescript-eslint/naming-convention */ +/* eslint-disable @typescript-eslint/naming-convention -- Many of the Discourse properties use snake case. */ interface ForumData { users: Array<{ id: number; diff --git a/src/modules/automod/hasKnownPhishingLink.ts b/src/modules/automod/hasKnownPhishingLink.ts index c64168d0..d17a01a6 100644 --- a/src/modules/automod/hasKnownPhishingLink.ts +++ b/src/modules/automod/hasKnownPhishingLink.ts @@ -27,48 +27,50 @@ export const hasKnownPhishingLink = async( const linksInMessage = content.matchAll(linkRegex); - for (const link of linksInMessage) { - const rawDomain = link.groups?.domain; - if (rawDomain === undefined) { - continue; - } - const domain = encodeURI(rawDomain); - const walshyRequest - // eslint-disable-next-line no-await-in-loop + const linkResults = await Promise.all( + [ ...linksInMessage ].map(async(link) => { + const rawDomain = link.groups?.domain; + if (rawDomain === undefined) { + return false; + } + const domain = encodeURI(rawDomain); + const walshyRequest + = await fetch("https://bad-domains.walshy.dev/check", { body: JSON.stringify({ domain }), headers: { - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention -- Header name requires dash. "X-Identity": "Rythm Moderation - built by naomi_lgbt", "accept": "application/json", }, method: "POST", }); - const walshyResponse - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, no-await-in-loop + const walshyResponse + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- .json() doesn't accept a generic. = (await walshyRequest.json()) as { badDomain: boolean }; - if (walshyResponse.badDomain) { - return true; - } - // eslint-disable-next-line no-await-in-loop - const yachtsRequest = await fetch( - `https://phish.sinking.yachts/v2/check/${domain}`, - { - headers: { - // eslint-disable-next-line @typescript-eslint/naming-convention - "X-Identity": "Rythm Moderation - built by naomi_lgbt", - "accept": "application/json", + if (walshyResponse.badDomain) { + return true; + } + + const yachtsRequest = await fetch( + `https://phish.sinking.yachts/v2/check/${domain}`, + { + headers: { + // eslint-disable-next-line @typescript-eslint/naming-convention -- Header name requires dash. + "X-Identity": "Rythm Moderation - built by naomi_lgbt", + "accept": "application/json", + }, }, - }, - ); - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, no-await-in-loop - const yachtsResult = (await yachtsRequest.json()) as boolean; - if (yachtsResult) { - return true; - } - return false; - } - return false; + ); + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- .json() doesn't accept a generic. + const yachtsResult = (await yachtsRequest.json()) as boolean; + if (yachtsResult) { + return true; + } + return false; + }), + ); + return linkResults.includes(true); } catch (error) { await errorHandler(bot, "phishing listener", error); return false; diff --git a/src/modules/automod/hasNonApprovedInvite.ts b/src/modules/automod/hasNonApprovedInvite.ts index 46d58107..3e7e679f 100644 --- a/src/modules/automod/hasNonApprovedInvite.ts +++ b/src/modules/automod/hasNonApprovedInvite.ts @@ -22,43 +22,44 @@ export const hasNonApprovedInvite = async( ): Promise => { try { const inviteRegex - // eslint-disable-next-line stylistic/max-len + // eslint-disable-next-line stylistic/max-len -- This regex is too long to be split. = /(?:https?:\/\/)?discord(?:(?:app)?\.com\/invite|\.gg)\/?(?[\dA-Za-z]+)(?:\?event=\d*)?\/?/g; const invitesInMessage = content. replaceAll(/\s/g, ""). matchAll(inviteRegex); - for (const invite of invitesInMessage) { - const slug = invite.groups?.slug; - if (slug === undefined) { - continue; - } - // eslint-disable-next-line no-await-in-loop - const request = await fetch( - `https://discord.com/api/v10/invites/${slug}`, - { - headers: { - accept: "application/json", + const invites = await Promise.all( + [ ...invitesInMessage ].map(async(invite) => { + const slug = invite.groups?.slug; + if (slug === undefined) { + return false; + } + const request = await fetch( + `https://discord.com/api/v10/invites/${slug}`, + { + headers: { + accept: "application/json", + }, + method: "GET", }, - method: "GET", - }, - ).catch(() => { - return null; - }); - const { guild_id: guildId } - // eslint-disable-next-line no-await-in-loop, @typescript-eslint/consistent-type-assertions, @typescript-eslint/naming-convention - = (await request?.json()) as { guild_id: string | undefined }; - if (guildId === undefined) { - continue; - } - const isApproved = approvedInviteIds.has(guildId); - if (!isApproved) { - return true; - } - } - - return false; + ).catch(() => { + return null; + }); + const { guild_id: guildId } + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions, @typescript-eslint/naming-convention -- .json() doesn't accept a generic. + = (await request?.json()) as { guild_id: string | undefined }; + if (guildId === undefined) { + return false; + } + const isApproved = approvedInviteIds.has(guildId); + if (!isApproved) { + return true; + } + return false; + }), + ); + return invites.some(Boolean); } catch (error) { await errorHandler(bot, "invite listener", error); return false; diff --git a/src/modules/formatter.ts b/src/modules/formatter.ts index 140266fe..fd7a7b41 100644 --- a/src/modules/formatter.ts +++ b/src/modules/formatter.ts @@ -1,6 +1,27 @@ import { format, type Options } from "prettier"; import stripAnsi from "strip-ansi"; +const errorIsPrettierError = ( + error: unknown, +): error is { + loc: { start: { line: number; column: number } }; + codeFrame: string; +} => { + return ( + typeof error === "object" + && error !== null + && "loc" in error + && typeof error.loc === "object" + && error.loc !== null + && "start" in error.loc + && typeof error.loc.start === "object" + && error.loc.start !== null + && "line" in error.loc.start + && "column" in error.loc.start + && "codeFrame" in error + ); +}; + /** * Formats user's unformatted code received from user's message. * @param unformattedCode - The unformatted code received from the user's message. @@ -50,13 +71,15 @@ export async function formatter( * the message, if sent to Discord, can't be displayed. That's why we use `stripAnsi` * in the next line to remove any ANSI provided by Prettier's parser */ - const formattedCode = await format(unformattedCode, options). - // eslint-disable-next-line @typescript-eslint/use-unknown-in-catch-callback-variable - catch((error) => { + const formattedCode = await format(unformattedCode, options).catch( + (error: unknown) => { + if (!errorIsPrettierError(error)) { + throw error; + } return stripAnsi( - // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access `SyntaxError: Unexpected token (${String(error.loc.start.line)}:${String(error.loc.start.column)})\n\n${String(error.codeFrame)}`, ); - }); + }, + ); return formattedCode; } diff --git a/src/modules/generateLogs.ts b/src/modules/generateLogs.ts index 1d0fe89e..0af65628 100644 --- a/src/modules/generateLogs.ts +++ b/src/modules/generateLogs.ts @@ -16,7 +16,7 @@ export const generateLogs = async( channelId: string, ): Promise => { try { - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- Dynamic delete is required here. delete camperChan.privateLogs[channelId]; const logs = await readFile( diff --git a/src/modules/generateProfileImage.ts b/src/modules/generateProfileImage.ts index 2ef92c8e..05f0afb7 100644 --- a/src/modules/generateProfileImage.ts +++ b/src/modules/generateProfileImage.ts @@ -1,4 +1,4 @@ -/* eslint-disable max-lines */ +/* eslint-disable max-lines -- Need to make this modules at some point. */ import { AttachmentBuilder } from "discord.js"; import nodeHtmlToImage from "node-html-to-image"; import { badges } from "../config/badges.js"; @@ -93,11 +93,11 @@ const getCertificationSection = ( }; } - // eslint-disable-next-line unicorn/no-array-reduce + // eslint-disable-next-line unicorn/no-array-reduce -- I'll rewrite this someday. const shouldMakeSVG = Object.entries(learnRecord).reduce( (accumulator: Array, [ key, value ]) => { if (key in generatorMap && value === true) { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- I know what I'm doing. accumulator.push(key as keyof typeof generatorMap); } return accumulator; @@ -130,7 +130,7 @@ const getbadgesSection = (record: levels): { html: string; alt: string } => { html: "

No badges earned yet.

", }; } - // eslint-disable-next-line unicorn/no-array-reduce + // eslint-disable-next-line unicorn/no-array-reduce -- I'll rewrite this someday. const processed = record.badges.reduce( (accumulator: { html: Array; alt: Array }, element) => { const isValid = badges.find((badge) => { @@ -370,7 +370,7 @@ const generateLeaderboardImage = async( ? "d0d0d5" : l.colour};padding: 2.5%;border-radius: 100px;">
diff --git a/src/modules/sendModerationDm.ts b/src/modules/sendModerationDm.ts index 565b2725..489695d2 100644 --- a/src/modules/sendModerationDm.ts +++ b/src/modules/sendModerationDm.ts @@ -7,21 +7,24 @@ import type { ModerationActions } from "../interfaces/moderationActions.js"; /** * Generates a moderation embed notice and sends it to the user. * @param camperChan - CamperChan's Discord instance. - * @param action - The moderation action taken. - * @param user - The Discord user being moderated. - * @param guildName - The name of the guild the moderation occurred in. - * @param reason - The reason for the moderation action. + * @param parameters - The parameters for the moderation action. + * @param parameters.action - The moderation action taken. + * @param parameters.user - The Discord user being moderated. + * @param parameters.guildName - The name of the guild the moderation occurred in. + * @param parameters.reason - The reason for the moderation action. * @returns True if the message was sent, false otherwise. */ export const sendModerationDm = async( camperChan: ExtendedClient, - action: ModerationActions, - user: User, - guildName: string, - reason: string, -// eslint-disable-next-line @typescript-eslint/max-params + parameters: { + action: ModerationActions; + user: User; + guildName: string; + reason: string; + }, ): Promise => { try { + const { action, user, guildName, reason } = parameters; const embed = new EmbedBuilder(); embed.setTitle(`${action} Notification!`); embed.setDescription( @@ -33,9 +36,8 @@ export const sendModerationDm = async( if (action === "ban") { embed.addFields({ - name: "Appeals", - value: - `You can use [this google form](https://docs.google.com/forms/d/e/1FAIpQLSdhJjpK8dPlktQMEatUwmXworqZF9ig14oiwmiZzd0skz5ekQ/viewform) to appeal your ban after you have [read our code of conduct](https://www.freecodecamp.org/news/code-of-conduct).`, + name: "Appeals", + value: `You can use [this google form](https://docs.google.com/forms/d/e/1FAIpQLSdhJjpK8dPlktQMEatUwmXworqZF9ig14oiwmiZzd0skz5ekQ/viewform) to appeal your ban after you have [read our code of conduct](https://www.freecodecamp.org/news/code-of-conduct).`, }); } diff --git a/src/server/serve.ts b/src/server/serve.ts index c628e114..e4a3858b 100644 --- a/src/server/serve.ts +++ b/src/server/serve.ts @@ -78,10 +78,10 @@ export const instantiateServer = async( await response.status(200).send("OK~!"); if (event === "pull_request") { - // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-assertions + // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/consistent-type-assertions -- I'll make a type guard at some point. const { action, pull_request, label } = request.body as { action: string; - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention -- Github API name. pull_request: { title: string; html_url: string; number: number }; label?: { name: string }; }; @@ -91,25 +91,25 @@ export const instantiateServer = async( } await camperChan.octokit.rest.pulls.createReview({ body: action === "labeled" - // eslint-disable-next-line stylistic/max-len + // eslint-disable-next-line stylistic/max-len -- It's a string. ? "This PR has been marked as DO NOT MERGE. When you are ready to merge it, remove the label and I'll unblock the PR." - // eslint-disable-next-line stylistic/max-len + // eslint-disable-next-line stylistic/max-len -- It's a string. : "This PR has been unmarked as DO NOT MERGE. You may now merge this PR.", event: action === "labeled" ? "REQUEST_CHANGES" : "APPROVE", owner: "nhcarrigan", - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention -- Github API. pull_number: pull_request.number, repo: "camperchan", }); } if (event === "issues") { - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- I'll make a type guard at some point. const { action, issue, label } = request.body as { action: string; - // eslint-disable-next-line @typescript-eslint/naming-convention + // eslint-disable-next-line @typescript-eslint/naming-convention -- Github API name. issue: { title: string; html_url: string; number: number }; label: { name: string }; }; diff --git a/src/utils/fetchLearnRecord.ts b/src/utils/fetchLearnRecord.ts index 475a0452..557b59b2 100644 --- a/src/utils/fetchLearnRecord.ts +++ b/src/utils/fetchLearnRecord.ts @@ -24,7 +24,7 @@ export const fetchLearnRecord = async( ? cached : null; } - // eslint-disable-next-line @typescript-eslint/no-dynamic-delete + // eslint-disable-next-line @typescript-eslint/no-dynamic-delete -- Dynamic delete is required here. delete camperChan.learnAccounts[userId]; const { PROD_URI: productionUri, diff --git a/src/utils/formatText.ts b/src/utils/formatText.ts index 43208801..bd3ef7dc 100644 --- a/src/utils/formatText.ts +++ b/src/utils/formatText.ts @@ -52,7 +52,7 @@ export const formatTextToTable = ( const baseColumnWidths = headers.map((header) => { return String(header).length; }); - // eslint-disable-next-line unicorn/no-array-reduce + // eslint-disable-next-line unicorn/no-array-reduce -- I'll fix this at some point. const columnWidths = rows.reduce((accumulator: Array, row) => { for (const [ index, column ] of row.entries()) { const currentColumnLength = String(column).length; diff --git a/src/utils/loadCommands.ts b/src/utils/loadCommands.ts index 82706705..a38b5b97 100644 --- a/src/utils/loadCommands.ts +++ b/src/utils/loadCommands.ts @@ -1,7 +1,3 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/consistent-type-assertions */ -/* eslint-disable no-await-in-loop */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ import { readdir, stat } from "node:fs/promises"; import { join } from "node:path"; import { errorHandler } from "./errorHandler.js"; @@ -9,6 +5,17 @@ import { logHandler } from "./logHandler.js"; import type { Command } from "../interfaces/command.js"; import type { ExtendedClient } from "../interfaces/extendedClient.js"; +const isModule = (module: unknown): module is Record => { + return module !== null && typeof module === "object"; +}; + +const isCommand += (module: unknown, name: string): module is Record => { + return isModule(module) && name in module + && typeof module[name] === "object" + && "run" in module[name] && "data" in module[name]; +}; + /** * Reads the `/commands` directory and dynamically imports the files, * then pushes the imported data to an array. @@ -19,26 +26,30 @@ export const loadCommands = async( camperChan: ExtendedClient, ): Promise> => { try { - const result: Array = []; const files = await readdir( join(process.cwd(), "prod", "commands"), "utf-8", ); - for (const file of files) { + const result = await Promise.all(files.map(async(file) => { const status = await stat(join(process.cwd(), "prod", "commands", file)); if (status.isDirectory()) { - continue; + return null; } const [ name ] = file.split("."); if (name === undefined) { logHandler.error(`Cannot find name from ${file}.`); - continue; + return null; } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Dynamic import. const module = await import(join(process.cwd(), "prod", "commands", file)); - result.push(module[name] as Command); - } - return result; + return isCommand(module, name) + ? module[name] + : null; + })); + return result.filter((r): r is Command => { + return r !== null; + }); } catch (error) { await errorHandler(camperChan, "load commands module", error); return []; diff --git a/src/utils/loadContexts.ts b/src/utils/loadContexts.ts index 9cfccfb4..ce1f44dd 100644 --- a/src/utils/loadContexts.ts +++ b/src/utils/loadContexts.ts @@ -1,40 +1,55 @@ -/* eslint-disable @typescript-eslint/no-unsafe-member-access */ -/* eslint-disable @typescript-eslint/consistent-type-assertions */ -/* eslint-disable no-await-in-loop */ -/* eslint-disable @typescript-eslint/no-unsafe-assignment */ -import { readdir } from "node:fs/promises"; +import { readdir, stat } from "node:fs/promises"; import { join } from "node:path"; import { errorHandler } from "./errorHandler.js"; import { logHandler } from "./logHandler.js"; import type { Context } from "../interfaces/context.js"; import type { ExtendedClient } from "../interfaces/extendedClient.js"; +const isModule = (module: unknown): module is Record => { + return module !== null && typeof module === "object"; +}; + +const isCommand += (module: unknown, name: string): module is Record => { + return isModule(module) && name in module + && typeof module[name] === "object" + && "run" in module[name] && "data" in module[name]; +}; + /** * Reads the `/contexts` directory and dynamically imports the files, * then pushes the imported data to an array. * @param camperChan - CamperChan's Discord instance. - * @returns Array of Context objects representing the imported commands. + * @returns Array of Command objects representing the imported commands. */ export const loadContexts = async( camperChan: ExtendedClient, ): Promise> => { try { - const result: Array = []; const files = await readdir( join(process.cwd(), "prod", "contexts"), "utf-8", ); - for (const file of files) { + const result = await Promise.all(files.map(async(file) => { + const status = await stat(join(process.cwd(), "prod", "contexts", file)); + if (status.isDirectory()) { + return null; + } const [ name ] = file.split("."); if (name === undefined) { logHandler.error(`Cannot find name from ${file}.`); - continue; + return null; } + // eslint-disable-next-line @typescript-eslint/no-unsafe-assignment -- Dynamic import. const module = await import(join(process.cwd(), "prod", "contexts", file)); - result.push(module[name] as Context); - } - return result; + return isCommand(module, name) + ? module[name] + : null; + })); + return result.filter((r): r is Context => { + return r !== null; + }); } catch (error) { await errorHandler(camperChan, "load contexts module", error); return []; diff --git a/src/utils/loadQuotes.ts b/src/utils/loadQuotes.ts index 670b4e9f..7e24f694 100644 --- a/src/utils/loadQuotes.ts +++ b/src/utils/loadQuotes.ts @@ -13,7 +13,7 @@ export const loadQuotes const quoteFetch = await fetch( `https://raw.githubusercontent.com/freeCodeCamp/freeCodeCamp/main/client/i18n/locales/english/motivation.json`, ); - // eslint-disable-next-line @typescript-eslint/consistent-type-assertions + // eslint-disable-next-line @typescript-eslint/consistent-type-assertions -- It kills me that .json() doesn't take a generic. const quoteData = (await quoteFetch.json()) as QuoteList; return quoteData; } catch (error) { diff --git a/src/utils/registerCommands.ts b/src/utils/registerCommands.ts index eab00518..66637126 100644 --- a/src/utils/registerCommands.ts +++ b/src/utils/registerCommands.ts @@ -1,5 +1,6 @@ import { REST, + type RESTOptions, type RESTPostAPIApplicationCommandsJSONBody, type RESTPostAPIChatInputApplicationCommandsJSONBody, Routes, @@ -18,10 +19,10 @@ import type { ExtendedClient } from "../interfaces/extendedClient.js"; */ export const registerCommands = async( camperChan: ExtendedClient, - restClass = REST, + restClass: new (options: Partial)=> REST = REST, ): Promise => { try { - // eslint-disable-next-line new-cap + // eslint-disable-next-line new-cap -- This is a class constructor. const rest = new restClass({ version: "10" }).setToken( camperChan.config.token, ); diff --git a/test/commands/subcommands/community/handleCodeOfConduct.spec.ts b/test/commands/subcommands/community/handleCodeOfConduct.spec.ts index df6348d0..320aa187 100644 --- a/test/commands/subcommands/community/handleCodeOfConduct.spec.ts +++ b/test/commands/subcommands/community/handleCodeOfConduct.spec.ts @@ -1,4 +1,3 @@ -/* eslint-disable stylistic/max-len */ import { ChannelType, type EmbedBuilder } from "discord.js"; import { MockChannel, @@ -8,7 +7,8 @@ import { MockUser, } from "discordjs-testing"; import { describe, assert, it } from "vitest"; -import { handleCodeOfConduct } from "../../../../src/commands/subcommands/community/handleCodeOfConduct.js"; +import { handleCodeOfConduct } + from "../../../../src/commands/subcommands/community/handleCodeOfConduct.js"; const guild = new MockGuild({ name: "it Guild", @@ -54,22 +54,26 @@ describe("codeOfConduct Handler", () => { assert.equal(embed.data.title, "freeCodeCamp Code of Conduct"); assert.equal( embed.data.description, + // eslint-disable-next-line stylistic/max-len -- It's a long string. "These are the basic rules for interacting with the FreeCodeCamp community on any platform, including this Discord server. You can read the full document on the [FreeCodeCamp article](https://freecodecamp.org/news/code-of-conduct)", ); const [ first, second, third ] = embed.data.fields || []; assert.equal(first.name, "No harassment"); assert.equal( first.value, + // eslint-disable-next-line stylistic/max-len -- It's a long string. "Harassment includes sexual language and imagery, deliberate intimidation, stalking, name-calling, unwelcome attention, libel, and any malicious hacking or social engineering. freeCodeCamp should be a harassment-free experience for everyone, regardless of gender, gender identity and expression, age, sexual orientation, disability, physical appearance, body size, race, national origin, or religion (or lack thereof).", ); assert.equal(second.name, "No trolling"); assert.equal( second.value, + // eslint-disable-next-line stylistic/max-len -- It's a long string. "Trolling includes posting inflammatory comments to provoke an emotional response or disrupt discussions.", ); assert.equal(third.name, "No spamming"); assert.equal( third.value, + // eslint-disable-next-line stylistic/max-len -- It's a long string. "Spamming includes posting off-topic messages to disrupt discussions, promoting a product, soliciting donations, advertising a job / internship / gig, or flooding discussions with files or text.", ); assert.equal( diff --git a/test/utils/loadCommands.spec.ts b/test/utils/loadCommands.spec.ts index 6189ab34..b87386b5 100644 --- a/test/utils/loadCommands.spec.ts +++ b/test/utils/loadCommands.spec.ts @@ -36,7 +36,7 @@ describe("loadCommands", () => { (command) => { return command.data.name. split("-"). - // eslint-disable-next-line unicorn/no-array-reduce + // eslint-disable-next-line unicorn/no-array-reduce -- Testy testy, won't refactor probably. reduce( (accumulator, element, index) => { return index === 0