From 20a20913f8346259f97cacc52e392c1953458f1c Mon Sep 17 00:00:00 2001 From: Flleeppyy Date: Sun, 21 Jul 2024 05:20:16 -0700 Subject: [PATCH] feat: ""working"" it runs. that's all. nothing fancy. ill work on this later when i can... hopefully. --- .github/funding.yml | 2 -- config.example.ts | 7 ++-- eslint.config.js | 46 ++++++++++++++++++++++++++ src/classes/Client.ts | 37 ++++++++++++++------- src/classes/Command.ts | 11 +++++-- src/classes/Event.ts | 5 ++- src/classes/GithubManager.ts | 47 ++++++++++++++++++++++++++ src/classes/LocaleSystem.ts | 64 ++++++++++++++++++++++-------------- src/events/Interaction.ts | 13 +++----- src/events/Message.ts | 5 +-- src/typings/config.d.ts | 9 +++-- src/typings/locales.d.ts | 6 +--- src/utils/constants.ts | 2 ++ src/utils/fetch.ts | 4 +-- src/utils/loader.ts | 20 +++++------ src/utils/timestamp.ts | 2 +- src/utils/validator.ts | 2 +- 17 files changed, 207 insertions(+), 75 deletions(-) delete mode 100644 .github/funding.yml create mode 100644 eslint.config.js create mode 100644 src/classes/GithubManager.ts diff --git a/.github/funding.yml b/.github/funding.yml deleted file mode 100644 index 3475211..0000000 --- a/.github/funding.yml +++ /dev/null @@ -1,2 +0,0 @@ -ko_fi: sysdotini -github: sysdotini diff --git a/config.example.ts b/config.example.ts index 2794f8c..2d1d3a7 100644 --- a/config.example.ts +++ b/config.example.ts @@ -3,8 +3,11 @@ const config: HibikiConfig = { defaultLocale: "en-GB", clientOptions: {}, githubToken: "", - guildId: "", - forumId: "", + githubManagerConfig: { + guildId: "", + forumId: "", + repoString: "", + }, colours: { primary: 0x64_8f_ff, diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 0000000..87e5523 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,46 @@ +import ks from "eslint-config-ks"; + +export default ks( + { + prettier: true, + json: true, + typescript: true, + project: ["tsconfig.json"], + }, + [ + { + ignores: ["src/utils/dbSchemaParser.ts"], + files: ["src/**/*.ts"], + rules: { + "prettier/prettier": "warn", + "security/detect-object-injection": "off", + "import/no-named-as-default-member": "off", + "max-depth": ["error", 6], + "unicorn/prefer-ternary": "off", + "@typescript-eslint/no-unused-vars": [ + "error", + { + args: "all", + argsIgnorePattern: "^_", + caughtErrors: "all", + caughtErrorsIgnorePattern: "^_", + destructuredArrayIgnorePattern: "^_", + varsIgnorePattern: "^_", + ignoreRestSiblings: true, + }, + ], + "@typescript-eslint/no-unnecessary-condition": "warn", + "unicorn/no-await-expression-member": "warn", + "unicorn/no-null": "off", + "@typescript-eslint/naming-convention": [ + "warn", + { + format: ["PascalCase"], + trailingUnderscore: "allow", + selector: "typeLike", + }, + ], + }, + }, + ], +); diff --git a/src/classes/Client.ts b/src/classes/Client.ts index d27bde3..2d6dd74 100644 --- a/src/classes/Client.ts +++ b/src/classes/Client.ts @@ -1,17 +1,22 @@ +/* eslint-disable @typescript-eslint/restrict-template-expressions */ +/* eslint-disable @typescript-eslint/no-misused-promises */ /** * @file Client * @description Connects to Discord and handles global functions */ -import type { HibikiCommand } from "./Command.js"; -import type { HibikiEvent } from "./Event.js"; -import type { HibikiLogger } from "./Logger.js"; +import path from "node:path"; +import url from "node:url"; + +import { Client, Collection, GatewayIntentBits } from "discord.js"; + import { loadCommands, loadEvents, registerSlashCommands } from "../utils/loader.js"; import { logger } from "../utils/logger.js"; +import type { HibikiCommand } from "./Command.js"; +import type { HibikiEvent } from "./Event.js"; +import { GithubManager } from "./GithubManager.js"; import { HibikiLocaleSystem } from "./LocaleSystem.js"; -import { Client, Collection, GatewayIntentBits } from "discord.js"; -import path from "node:path"; -import url from "node:url"; +import type { HibikiLogger } from "./Logger.js"; // Are we being sane or not? const IS_PRODUCTION = process.env.NODE_ENV === "production"; @@ -40,11 +45,16 @@ export class HibikiClient extends Client { // Hibiki's locale system readonly localeSystem: HibikiLocaleSystem; + // Github manager + readonly githubManager: GithubManager; + constructor(config: HibikiConfig) { super({ ...config.clientOptions, intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages] }); this.config = config; + this.githubManager = new GithubManager(this, this.config.githubManagerConfig.repoString); + // Creates a new Locale System engine this.localeSystem = new HibikiLocaleSystem(LOCALES_DIRECTORY, this.config.defaultLocale); } @@ -56,7 +66,7 @@ export class HibikiClient extends Client { public init() { try { // Logs into the Discord API, I guess - this.login(this.config.token); + void this.login(this.config.token); // Wait for the initial login before loading modules this.once("ready", async () => { @@ -65,12 +75,17 @@ export class HibikiClient extends Client { await loadEvents(this, EVENTS_DIRECTORY); await loadEvents(this, LOGGERS_DIRECTORY, true); + await this.githubManager.init({ + forumId: this.config.githubManagerConfig.forumId, + guildId: this.config.githubManagerConfig.guildId, + }); + // Registers commands - registerSlashCommands(this); + await registerSlashCommands(this); - logger.info(`Logged in as ${this.user?.tag} in ${this.guilds.cache.size} guilds on shard #${this.shard?.ids[0]}`); - logger.info(`${this.commands.size} commands loaded on shard #${this.shard?.ids[0]}`); - logger.info(`${this.events.size} events loaded on shard #${this.shard?.ids[0]}`); + logger.info(`Logged in as ${this.user?.tag} in ${this.guilds.cache.size} guilds`); + logger.info(`${this.commands.size} commands loaded`); + logger.info(`${this.events.size} events loaded`); }); } catch (error) { logger.error(`An error occured while initializing Hibiki: ${error}`); diff --git a/src/classes/Command.ts b/src/classes/Command.ts index 2cb1efd..6069728 100644 --- a/src/classes/Command.ts +++ b/src/classes/Command.ts @@ -4,9 +4,10 @@ * @module HibikiCommand */ -import type { HibikiClient } from "./Client.js"; -import type { APIApplicationCommandOption } from "discord-api-types/v10"; import type { ChatInputCommandInteraction, Message } from "discord.js"; +import type { APIApplicationCommandOption } from "discord-api-types/v10"; + +import type { HibikiClient } from "./Client.js"; /** * Hibiki command data in JSON form for slash command registration @@ -63,7 +64,11 @@ export abstract class HibikiCommand { * @param category The command category (matches the directory) */ - protected constructor(protected bot: HibikiClient, public name: string, public category: string) {} + protected constructor( + protected bot: HibikiClient, + public name: string, + public category: string, + ) {} /** * Converts a Hibiki command to Discord API-compatible JSON diff --git a/src/classes/Event.ts b/src/classes/Event.ts index c3c75a9..430207c 100644 --- a/src/classes/Event.ts +++ b/src/classes/Event.ts @@ -24,7 +24,10 @@ export abstract class HibikiEvent { * @param name The event name, matching the filename */ - constructor(protected bot: HibikiClient, public name: string) {} + constructor( + protected bot: HibikiClient, + public name: string, + ) {} /** * Runs an event diff --git a/src/classes/GithubManager.ts b/src/classes/GithubManager.ts new file mode 100644 index 0000000..572ce44 --- /dev/null +++ b/src/classes/GithubManager.ts @@ -0,0 +1,47 @@ +import { Octokit } from "@octokit/rest"; +import { ChannelType, ForumChannel, Guild } from "discord.js"; +import { hibikiUserAgent } from "utils/constants.js"; + +import { HibikiClient } from "./Client.js"; + +export class GithubManager { + private bot: HibikiClient; + + private octokit: Octokit; + + repoOwner: string; + repo: string; + + forumChannel?: ForumChannel; + guild?: Guild; + + /** + * + * @param bot Hibiki client + * @param repo Repository path, example: Blahblah/my-repo + */ + constructor(bot: HibikiClient, repo: string) { + this.bot = bot; + + this.repoOwner = repo.split("/")[0]; + this.repo = repo.split("/")[1]; + + this.octokit = new Octokit({ + userAgent: hibikiUserAgent, + }); + } + + public init(config: { guildId: string; forumId: string }) { + // Resolve the guild! + const guild = this.bot.guilds.resolve(config.guildId); + if (!guild) throw new Error("Failed to resolve guild " + config.guildId + "!"); + + const forum = guild.channels.resolve(config.forumId); + + if (!forum) throw new Error("Failed to resolve forum channel " + config.forumId + "!"); + if (forum.type !== ChannelType.GuildForum) throw new Error("The channel " + forum.name + "is NOT a forum channel!"); + + this.guild = guild; + this.forumChannel = forum; + } +} diff --git a/src/classes/LocaleSystem.ts b/src/classes/LocaleSystem.ts index a89f5c8..ac9f172 100644 --- a/src/classes/LocaleSystem.ts +++ b/src/classes/LocaleSystem.ts @@ -1,23 +1,32 @@ +/* eslint-disable @typescript-eslint/no-unnecessary-condition */ /** * @file LocaleSystem * @description Handles loading, parsing, and getting locales * @module HibikiLocaleSystem */ +// We aren't handling user input except in rare cases. +/* eslint-disable security/detect-non-literal-regexp */ + +// Same reason as previous. +/* eslint-disable security/detect-non-literal-fs-filename */ + /** * @TODO: https://discord.com/developers/docs/interactions/application-commands#localization * Localise our slash command stuff... */ +import fs, { type PathLike } from "node:fs"; + import type { getString, HibikiLocaleStrings } from "../typings/locales.js"; -import type { HibikiClient } from "./Client.js"; -import type { PathLike } from "node:fs"; import { logger } from "../utils/logger.js"; -import fs from "node:fs"; +import type { HibikiClient } from "./Client.js"; export class HibikiLocaleSystem { // A JSON object containing locale names & strings - readonly locales: { [k: string]: { [k: string]: any } } = {}; + // Setting this to anything other than any just creates more type erorrs than I'd like to deal with. TODO: Deal with these types + // eslint-disable-next-line @typescript-eslint/no-explicit-any + readonly locales: { [k: string]: { [k: string]: any } | undefined } = {}; // The default locale to fall back to readonly defaultLocale: HibikiLocaleCode; @@ -40,9 +49,15 @@ export class HibikiLocaleSystem { * @param args Any args to pass to the string */ - public getLocale(language: HibikiLocaleCode, fieldName: HibikiLocaleStrings, args?: { [x: string]: any }): string { + public getLocale( + language: HibikiLocaleCode, + fieldName: HibikiLocaleStrings, + // TODO: change to string | number | undefined, and deal with types accordingly for plurals + // eslint-disable-next-line @typescript-eslint/no-explicit-any + args?: { [x: string]: any }, + ): string { const category = fieldName.split("."); - let output = ""; + let output: string | undefined = ""; // Gets the string output using the specified locale output = this._findLocaleString(language, fieldName, category); @@ -56,7 +71,7 @@ export class HibikiLocaleSystem { const isDefault = language === this.defaultLocale; // Throws the error with what field is missing - if (language) logger.warn(`${fieldName} is missing in the string table for ${language}.`); + logger.warn(`${fieldName} is missing in the string table for ${language}.`); return isDefault ? fieldName : this.getLocale(this.defaultLocale, fieldName, args); } @@ -77,7 +92,7 @@ export class HibikiLocaleSystem { const isOptional = optionalArgumentRegex.exec(output); // Optional argument support - if (isOptional) output = output.replace(isOptional[1], args[argument] === undefined ? "" : isOptional[2]); + if (isOptional) output = output.replace(isOptional[1], args[argument] === undefined ? "" : isOptional[2].toString()); output = output.replace(argumentRegex, args[argument]); // Checks to see if there are any plurals in the string @@ -94,11 +109,11 @@ export class HibikiLocaleSystem { // Fixes up plural output = output.replace(plurals[0], plural); - } else if (!plurals) { + } else { // Replaces dummy arguments with provided ones output = output.replace(`{${argument}}`, args[argument]); } - }; + } } // A regex to check to see if any optional arguments were provided @@ -118,15 +133,17 @@ export class HibikiLocaleSystem { */ public getLocaleFunction(language: HibikiLocaleCode): getString { - return (fieldName: HibikiLocaleStrings, args?: Record) => this.getLocale(language, fieldName, args); + return (fieldName: HibikiLocaleStrings, args?: Record) => this.getLocale(language, fieldName, args); } /** - * Returns what locale a user uses + * Returns what locale a user uses. + * Originally intended to do a database check, not sure what to do with it, + * so it will be left alone. * @param user The User ID to search for a set locale for */ - public async getUserLocale(user: DiscordSnowflake, bot: HibikiClient) { + public getUserLocale(_user: DiscordSnowflake, _bot: HibikiClient) { return this.defaultLocale; } @@ -140,13 +157,13 @@ export class HibikiLocaleSystem { for (const file of files) { // Scans each locale file and loads it - if (file.isDirectory()) this._loadLocales(`${path}/${file.name}`); + if (file.isDirectory()) this._loadLocales(`${path.toString()}/${file.name}`); else if (file.isFile()) { // Reads the actual locale file - const subfile = fs.readFileSync(`${path}/${file.name}`)?.toString(); + const subfile = fs.readFileSync(`${path.toString()}/${file.name}`).toString(); // Creates an empty locale object - const localeObject: Record = {}; + const localeObject: Record = {}; // Parses the locale's JSON const data: Record = JSON.parse(subfile); @@ -159,8 +176,7 @@ export class HibikiLocaleSystem { // Reads each string in the category for (const string of Object.entries(locale[1])) { - if ((string as [string, string])[1].length > 0) - localeObject[locale[0]][string[0]] = string[1]; + if ((string as [string, string])[1].length > 0) (localeObject[locale[0]] as Record)[string[0]] = string[1]; } } else { // Replaces empty strings @@ -181,12 +197,12 @@ export class HibikiLocaleSystem { * @param category The categories to search in */ - private _findLocaleString(language: HibikiLocaleCode, fieldName: HibikiLocaleStrings, category: string[]) { - if (!this.locales?.[language]) return; - let output; + private _findLocaleString(language: HibikiLocaleCode, fieldName: HibikiLocaleStrings, category: string[]): string | undefined { + if (!this.locales[language]) return; + let output: string | undefined; // Attempts to find the string if the category isn't provided - if (!this.locales?.[language]?.[category[0]] && !this.locales?.[fieldName]) { + if (!this.locales[language][category[0]] && !this.locales[fieldName]) { // Looks through each language for (const localeCategory of Object.getOwnPropertyNames(this.locales[language])) { // Looks through the categories @@ -196,10 +212,10 @@ export class HibikiLocaleSystem { } // Sets the output if the category exists - } else if (this.locales?.[language]?.[category[0]] && this.locales?.[language]?.[category[0]]?.[category[1]]) { + } else if (this.locales[language][category[0]] && this.locales[language][category[0]]?.[category[1]]) { output = this.locales[language][category[0]][category[1]]; // Sets the locale if no category exists - } else if (this.locales?.[language]?.[fieldName]) { + } else if (this.locales[language][fieldName]) { output = this.locales[language][fieldName]; } diff --git a/src/events/Interaction.ts b/src/events/Interaction.ts index eb9db32..b08f7f5 100644 --- a/src/events/Interaction.ts +++ b/src/events/Interaction.ts @@ -4,9 +4,10 @@ * @module HibikiInteractionEvent */ -import { EmbedBuilder, type ChatInputCommandInteraction } from "discord.js"; -import { logger } from "../utils/logger.js"; +import { type ChatInputCommandInteraction, EmbedBuilder } from "discord.js"; + import { HibikiEvent } from "../classes/Event.js"; +import { logger } from "../utils/logger.js"; export class HibikiInteractionEvent extends HibikiEvent { events: HibikiEventEmitter[] = ["interactionCreate"]; @@ -35,16 +36,12 @@ export class HibikiInteractionEvent extends HibikiEvent { }); await command.runWithInteraction?.(interaction); } catch (error) { - logger.error( - `An error occured running the command ${command.name}\n${(error as Error).stack}`, - ); + logger.error(`An error occured running the command ${command.name}\n${(error as Error).stack}`); await interaction.followUp({ embeds: [ new EmbedBuilder() .setTitle("Error") - .setDescription( - "An error occured running the command\n```" + (error as Error).stack + "```", - ) + .setDescription("An error occured running the command\n```" + (error as Error).stack + "```") .setColor(this.bot.config.colours.error), ], }); diff --git a/src/events/Message.ts b/src/events/Message.ts index ab1a57d..80f5996 100644 --- a/src/events/Message.ts +++ b/src/events/Message.ts @@ -4,15 +4,16 @@ * @module HibikiMessageEvent */ -import { HibikiEvent } from "../classes/Event.js"; import { ChannelType, Message } from "discord.js"; +import { HibikiEvent } from "../classes/Event.js"; + export class HibikiMessageEvent extends HibikiEvent { events: HibikiEventEmitter[] = ["messageCreate"]; requiredIntents?: ResolvableIntentString[] = ["GuildMessages"]; public async run(_event: HibikiEventEmitter, msg: Message) { - if (!msg?.content || !msg.author || msg.author.bot || !msg.channel || msg.channel.type !== ChannelType.GuildText) return; + if (!msg.content || !msg.author || msg.author.bot || !msg.channel || msg.channel.type !== ChannelType.GuildText) return; console.log(msg.content); } } diff --git a/src/typings/config.d.ts b/src/typings/config.d.ts index 31c792c..22cf17f 100644 --- a/src/typings/config.d.ts +++ b/src/typings/config.d.ts @@ -12,9 +12,12 @@ interface HibikiConfig { githubToken: string; - guildId: string - - forumId: string + githubManagerConfig: { + guildId: string; + forumId: string; + // Example: blahlbah/myrepo + repoString: string; + }; // The default locale to use defaultLocale: HibikiLocaleCode; diff --git a/src/typings/locales.d.ts b/src/typings/locales.d.ts index 85994f3..be9ea67 100644 --- a/src/typings/locales.d.ts +++ b/src/typings/locales.d.ts @@ -7,11 +7,7 @@ import * as defaultLocaleFile from "../locales/en-GB.json"; const en = defaultLocaleFile.default; -type HibikiLocaleStrings = - | "NAME" - | `github.${keyof typeof en.github}` - | `general.${keyof typeof en.general}` - | `global.${keyof typeof en.global}` +type HibikiLocaleStrings = "NAME" | `github.${keyof typeof en.github}` | `global.${keyof typeof en.global}`; // Type for getLocaleFunction() type getString = { diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 47da8e6..8eec81e 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -12,3 +12,5 @@ export const slashCommandNameRegex = /^[-_\p{L}\p{N}\p{sc=Deva}\p{sc=Thai}]{1,32 // The version of Hibiki currently running export const hibikiVersion = process.env.npm_package_version ?? "development"; + +export const hibikiUserAgent = `monkeprrelay/${hibikiVersion} (https://github.com/Monkestation/github-pr-relay)`; diff --git a/src/utils/fetch.ts b/src/utils/fetch.ts index 9ee5230..af8ff92 100644 --- a/src/utils/fetch.ts +++ b/src/utils/fetch.ts @@ -4,7 +4,7 @@ * @module fetch */ -import { hibikiVersion } from "./constants.js"; +import { hibikiUserAgent } from "./constants.js"; /** * Wraps around fetch() and adds our User-Agent, etc @@ -20,7 +20,7 @@ export default async (url: string, options?: RequestInit): Promise) { for (const eventToListenOn of events.values()) { for (const individualEvent of eventToListenOn.events) { - bot.on(individualEvent, (...eventParameters) => - eventToListenOn.run(individualEvent, ...eventParameters), - ); + bot.on(individualEvent, (...eventParameters) => eventToListenOn.run(individualEvent, ...eventParameters)); } } } diff --git a/src/utils/timestamp.ts b/src/utils/timestamp.ts index fc94dbd..cada97f 100644 --- a/src/utils/timestamp.ts +++ b/src/utils/timestamp.ts @@ -11,5 +11,5 @@ */ export function createFullTimestamp(time: Date) { - return ``; + return ``; } diff --git a/src/utils/validator.ts b/src/utils/validator.ts index 5530c64..af36aad 100644 --- a/src/utils/validator.ts +++ b/src/utils/validator.ts @@ -4,7 +4,7 @@ * @module validator */ -import { GatewayIntentsString, BitFieldResolvable, ClientOptions, IntentsBitField } from "discord.js"; +import { BitFieldResolvable, ClientOptions, GatewayIntentsString, IntentsBitField } from "discord.js"; /** * Checks to see if an intent is in config.options