diff --git a/eslint.config.mjs b/eslint.config.mjs index 9250d0b..145ad51 100644 --- a/eslint.config.mjs +++ b/eslint.config.mjs @@ -4,22 +4,31 @@ import eslint from "@eslint/js"; import tseslint from "typescript-eslint"; export default [ - { - ignores: [ - "./dist/**/*", - "./src/**/generated/**/*", - "./generated/**/*", - "esbuild.mjs", - "jest.config.js", - "postcss.config.js", - "tailwind.config.js", - ] - }, - ...tseslint.config( - eslint.configs.recommended, - tseslint.configs.recommended, { - files: ["./src/**/*.{ts,js}"] - } - ) + ignores: [ + "./dist/**/*", + "./src/**/generated/**/*", + "./generated/**/*", + "esbuild.mjs", + "jest.config.js", + "postcss.config.js", + "tailwind.config.js", + ] + }, + ...tseslint.config( + eslint.configs.recommended, + tseslint.configs.recommended, + { + files: ["./src/**/*.{ts,js}"], + rules: { + "@typescript-eslint/no-unused-vars": [ + "error", { + "argsIgnorePattern": "^_", + "varsIgnorePattern": "^_" + } + ], + "@typescript-eslint/no-explicit-any": "off" + } + } + ) ]; \ No newline at end of file diff --git a/prisma/seed.ts b/prisma/seed.ts index d03cf6b..587b7a2 100644 --- a/prisma/seed.ts +++ b/prisma/seed.ts @@ -1,4 +1,4 @@ -import Prisma, { PrismaClient } from "../generated/prisma"; +import { PrismaClient } from "../generated/prisma"; const client = new PrismaClient(); diff --git a/src/bot/__tests__/filters.test.ts b/src/bot/__tests__/filters.test.ts index 5371e8c..f3ee2ee 100644 --- a/src/bot/__tests__/filters.test.ts +++ b/src/bot/__tests__/filters.test.ts @@ -167,4 +167,16 @@ describe("Match: REGEX", () => { runFilterTest(filter, testCases); }); -describe("Match: FUZZY", () => { }); +describe("Match: FUZZY", () => { + const filter: prisma.Filter = { + ...templateFilter, + name: "", + value: String.raw``, + matching_algorithm: MatchingAlgorithms.REGEX, + is_insensitive: true + }; + + const testCases: FilterTestCase[] = []; + + runFilterTest(filter, testCases); +}); diff --git a/src/bot/bot.ts b/src/bot/bot.ts index 638f485..8bddcf0 100644 --- a/src/bot/bot.ts +++ b/src/bot/bot.ts @@ -1,7 +1,6 @@ import { Client, GatewayIntentBits } from "discord.js"; import EventHandler from "@bot/handlers/events"; import InteractionHandler from "@bot/handlers/interactions"; -import { triggerTask } from "@bot/task"; export default class DiscordBot extends Client { constructor() { diff --git a/src/bot/components/interaction.ts b/src/bot/components/interaction.ts index fd8592c..9288f79 100644 --- a/src/bot/components/interaction.ts +++ b/src/bot/components/interaction.ts @@ -1,11 +1,11 @@ -import { RESTPostAPIApplicationCommandsJSONBody, SlashCommandBuilder } from "discord.js"; +import { RESTPostAPIApplicationCommandsJSONBody, SlashCommandBuilder, ToAPIApplicationCommandOptions } from "discord.js"; import DiscordBot from "@bot/bot"; export default class Interaction { readonly client: DiscordBot; name!: string; description: string = "No description"; - options: any[] = []; + options: ToAPIApplicationCommandOptions[] = []; dmPermission!: boolean; constructor(client: DiscordBot) { diff --git a/src/bot/filter.ts b/src/bot/filter.ts index e4d8d77..3229af2 100644 --- a/src/bot/filter.ts +++ b/src/bot/filter.ts @@ -1,4 +1,4 @@ -import fuzz from "fuzzball"; // todo: implement for fuzzy match +import fuzz from "fuzzball"; import { Filter, MatchingAlgorithms } from "../../generated/prisma"; function splitWords(filterValue: string): string[] { @@ -60,7 +60,16 @@ export const regex = (filter: Filter, input: string) => { }; export const fuzzy = (filter: Filter, input: string) => { - throw new Error("'fuzzy' filter not implemented"); + let filterValue = filter.value.replace(/[^\w\s]/g, ""); + let inputValue = input.replace(/[^\w\s]/g, ""); + + if (filter.is_insensitive) { + filterValue = filterValue.toLowerCase(); + inputValue = inputValue.toLowerCase(); + } + + const ratio = fuzz.partial_ratio(filterValue, inputValue); + return ratio > 90; }; export const mapAlgorithmToFunction = (filter: Filter, input: string) => { diff --git a/src/bot/handlers/events.ts b/src/bot/handlers/events.ts index 5df6547..72c2bea 100644 --- a/src/bot/handlers/events.ts +++ b/src/bot/handlers/events.ts @@ -18,7 +18,8 @@ export default class EventHandler extends Collection { const modules = getAllFiles(eventsDirectory); for (const module of modules) { - const eventClass = ((r) => r.default || r)(require(module)); + const imported = await import(module); + const eventClass = imported.default || imported; const event: Event = new eventClass(this.client); this.set(event.name, event); diff --git a/src/bot/handlers/interactions.ts b/src/bot/handlers/interactions.ts index d7d7011..bfccbda 100644 --- a/src/bot/handlers/interactions.ts +++ b/src/bot/handlers/interactions.ts @@ -18,7 +18,8 @@ export default class InteractionHandler extends Collection const modules = getAllFiles(interactionsDirectory); for (const module of modules) { - const interactionClass = ((r) => r.default || r)(require(module)) + const imported = await import(module); + const interactionClass = imported.default || imported; const interaction: Interaction = new interactionClass(this.client); this.set(interaction.name, interaction); } @@ -28,7 +29,7 @@ export default class InteractionHandler extends Collection const interactions = this.map(inter => inter.toJSON()) const rest = new REST({ version: "10" }).setToken(process.env.BOT_TOKEN!); - for (const [_, guild] of this.client.guilds.cache) { + for (const guild of this.client.guilds.cache.values()) { rest.put( Routes.applicationGuildCommands(process.env.CLIENT_ID!, guild.id), { body: interactions } diff --git a/src/bot/task.ts b/src/bot/task.ts index a0cd016..4c1730c 100644 --- a/src/bot/task.ts +++ b/src/bot/task.ts @@ -29,10 +29,13 @@ const processGuild = async (guild: Guild, client: Client) => { }; const getParsedUrl = async (url: string) => { - const parser = new RssParser(); - - try { return parser.parseURL(url) } - catch (error) { return undefined } + try { + return new RssParser().parseURL(url) + } + catch (error) { + console.error(error); + return undefined + } }; const processFeed = async (feed: ExpandedFeed, client: Client) => { @@ -43,11 +46,11 @@ const processFeed = async (feed: ExpandedFeed, client: Client) => { for (const channelId of feed.channels.map(channel => channel.channel_id)) { const channel = client.channels.cache.get(channelId); - if (channel) await processItems(parsed.items, feed, channel, client); + if (channel) await processItems(parsed.items, feed, channel); } }; -const processItems = async (items: RssParser.Item[], feed: ExpandedFeed, channel: DiscordChannel, client: Client) => { +const processItems = async (items: RssParser.Item[], feed: ExpandedFeed, channel: DiscordChannel) => { console.log(`Processing ${items.length} items`); for (let i = items.length; i--;) { diff --git a/src/bot/utils/getAllFiles.ts b/src/bot/utils/getAllFiles.ts index f8e9429..f57e7d0 100644 --- a/src/bot/utils/getAllFiles.ts +++ b/src/bot/utils/getAllFiles.ts @@ -1,22 +1,29 @@ import fs from "fs"; import { join } from "path"; +/** + * Recursively gets all .ts or .js files in a directory and its subdirectories. + * @param directoryPath - The directory to start from. + * @param existingResult - Optional array to accumulate results. + * @returns A list of full paths to .ts or .js files. + */ const getAllFiles = (directoryPath: string, existingResult?: string[]) => { const fileNames = fs.readdirSync(directoryPath); - - let result: string[] = existingResult ?? []; + const result: string[] = existingResult ?? []; for (const fileName of fileNames) { - if (!(fileName.endsWith(".ts") || fileName.endsWith(".js"))) continue; - const fullPath = join(directoryPath, fileName); + const stat = fs.statSync(fullPath); + + if (stat.isDirectory()) { + getAllFiles(fullPath, result); + } else if (fileName.endsWith(".ts") || fileName.endsWith(".js")) { + result.push(fullPath); + } - fs.statSync(fullPath).isDirectory() - ? result = getAllFiles(fullPath, result) - : result.push(fullPath); } - return result + return result; }; export default getAllFiles; \ No newline at end of file diff --git a/src/client/src/ts/guild/feeds.ts b/src/client/src/ts/guild/feeds.ts index 6ceb339..b24473e 100644 --- a/src/client/src/ts/guild/feeds.ts +++ b/src/client/src/ts/guild/feeds.ts @@ -9,7 +9,7 @@ import "datatables.net-select-dt" import prisma from "../../../../../generated/prisma"; declare let guildId: string; -declare let channels: Array; +declare let channels: Array; // #region DataTable @@ -174,7 +174,7 @@ const columnDefs: ConfigColumnDefs[] = [ orderable: false, searchable: false, className: "size-px whitespace-nowrap", - render: (_data: unknown, type: string, row: any) => { + render: (_data: unknown, type: string, row: ExpandedFeed) => { if (!row.message_style || type !== "display") return null; const wrapper = $("
").addClass("flex px-6 py-4"); @@ -284,14 +284,23 @@ const onTableSelectChange = () => { $(".rows-selected-count-js").text(selectedRowsCount); const $elem = $(".rows-selected-count-js.zero-empty-js"); - selectedRowsCount === 0 ? $elem.hide() : $elem.show(); + if (selectedRowsCount === 0) { + $elem.hide(); + return; + } + + $elem.show(); }; $("#selectAllBox").on("change", function() { const dt: Api = (table as any).dataTable; - (this as HTMLInputElement).checked - ? dt.rows().select() - : dt.rows().deselect(); + + if ((this as HTMLInputElement).checked) { + dt.rows().select(); + return; + } + + dt.rows().deselect(); }); $("#deleteRowsBtn").on("click", async () => { @@ -353,9 +362,12 @@ const openEditModal = async (id: number | undefined) => { $("#editForm").removeClass("submitted"); editModal.open(); - id === undefined - ? clearEditModalData() - : loadEditModalData(id); + if (id === undefined) { + clearEditModalData(); + return; + } + + loadEditModalData(id); }; $(document).on("click", ".open-edit-modal-js", async event => { @@ -380,6 +392,7 @@ window.addEventListener("preline:ready", () => { interface ExpandedFeed extends prisma.Feed { channels: prisma.Channel[]; filters: prisma.Feed[]; + message_style: prisma.MessageStyle | undefined; } const clearEditModalData = () => { @@ -601,7 +614,7 @@ window.addEventListener("preline:ready", () => { styleSelect = new HSSelect(styleEl, styleSelectOptions); // Add options to the channel select - channels.forEach((channel: TextChannel) => { + channels.forEach(channel => { channelSelect.addOption({ title: channel.name, val: channel.id, diff --git a/src/client/src/ts/guild/filters.ts b/src/client/src/ts/guild/filters.ts index 15816f0..deeb500 100644 --- a/src/client/src/ts/guild/filters.ts +++ b/src/client/src/ts/guild/filters.ts @@ -212,16 +212,26 @@ const onTableSelectChange = () => { $(".rows-selected-count-js").text(selectedRowsCount); const $elem = $(".rows-selected-count-js.zero-empty-js"); - selectedRowsCount === 0 ? $elem.hide() : $elem.show(); + if (selectedRowsCount === 0) { + $elem.hide(); + return; + } + + $elem.show(); }; $("#selectAllBox").on("change", function() { const dt: Api = (table as any).dataTable; - (this as HTMLInputElement).checked - ? dt.rows().select() - : dt.rows().deselect(); + + if ((this as HTMLInputElement).checked) { + dt.rows().select(); + return; + } + + dt.rows().deselect(); }); + $("#deleteRowsBtn").on("click", async () => { const dt: Api = (table as any).dataTable; const rowsData = dt.rows({ selected: true }).data().toArray(); @@ -279,9 +289,12 @@ const openEditModal = async (id: number | undefined) => { $("#editForm").removeClass("submitted"); editModal.open(); - id === undefined - ? clearEditModalData() - : loadEditModalData(id); + if (id === undefined) { + clearEditModalData(); + return; + } + + loadEditModalData(id); }; $(document).on("click", ".open-edit-modal-js", async event => { diff --git a/src/client/src/ts/guild/styles.ts b/src/client/src/ts/guild/styles.ts index 92139cd..b8a2bb9 100644 --- a/src/client/src/ts/guild/styles.ts +++ b/src/client/src/ts/guild/styles.ts @@ -304,16 +304,26 @@ const onTableSelectChange = () => { $(".rows-selected-count-js").text(selectedRowsCount); const $elem = $(".rows-selected-count-js.zero-empty-js"); - selectedRowsCount === 0 ? $elem.hide() : $elem.show(); + if (selectedRowsCount === 0) { + $elem.hide(); + return; + } + + $elem.show(); }; $("#selectAllBox").on("change", function() { const dt: Api = (table as any).dataTable; - (this as HTMLInputElement).checked - ? dt.rows().select() - : dt.rows().deselect(); + + if ((this as HTMLInputElement).checked) { + dt.rows().select(); + return; + } + + dt.rows().deselect(); }); + $("#deleteRowsBtn").on("click", async () => { const dt: Api = (table as any).dataTable; const rowsData = dt.rows({ selected: true }).data().toArray(); @@ -373,9 +383,12 @@ const openEditModal = async (id: number | undefined) => { $("#editForm").removeClass("submitted"); editModal.open(); - id === undefined - ? clearEditModalData() - : loadEditModalData(id); + if (id === undefined) { + clearEditModalData(); + return; + } + + loadEditModalData(id); }; $(document).on("click", ".open-edit-modal-js", async event => { diff --git a/src/log.ts b/src/log.ts index 42e1c18..5dbcead 100644 --- a/src/log.ts +++ b/src/log.ts @@ -7,7 +7,7 @@ export const logger = winston.createLogger({ format: combine( timestamp({ format: timestampFormat }), json(), - printf(({ timestamp, level, message, ...data }) => { + printf(({ _timestamp, level, message, ...data }) => { const response = { level, message, data }; return JSON.stringify(response); })