diff --git a/package.json b/package.json index 7e9ce34..f1c2600 100644 --- a/package.json +++ b/package.json @@ -3,13 +3,14 @@ "version": "1.0.0", "license": "GPL-3.0", "scripts": { - "dev": "NODE_ENV=development tsx watch src/index.ts", + "dev": "cross-env NODE_ENV=development tsx watch src/index.ts", "start": "tsx src/index.ts", "reupload": "tsx src/_reupload.ts", "lint": "tsc && eslint ." }, "dependencies": { "@discordjs/rest": "1.7.1", + "colorette": "^2.0.20", "discord.js": "14.11.0", "tsx": "3.12.7" }, @@ -17,6 +18,7 @@ "@types/node": "20.3.1", "@typescript-eslint/eslint-plugin": "5.60.1", "@typescript-eslint/parser": "5.60.1", + "cross-env": "^7.0.3", "dotenv": "16.3.1", "eslint": "8.43.0", "gray-matter": "4.0.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 761eb42..2edd0c2 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -8,6 +8,9 @@ dependencies: '@discordjs/rest': specifier: 1.7.1 version: 1.7.1 + colorette: + specifier: ^2.0.20 + version: 2.0.20 discord.js: specifier: 14.11.0 version: 14.11.0 @@ -25,6 +28,9 @@ devDependencies: '@typescript-eslint/parser': specifier: 5.60.1 version: 5.60.1(eslint@8.43.0)(typescript@5.1.3) + cross-env: + specifier: ^7.0.3 + version: 7.0.3 dotenv: specifier: 16.3.1 version: 16.3.1 @@ -43,6 +49,11 @@ devDependencies: packages: + /@aashutoshrathi/word-wrap@1.2.6: + resolution: {integrity: sha512-1Yjs2SvM8TflER/OD3cOjhWWOZb58A2t7wpE2S9XfBYTiIl+XFhQG2bjy4Pu1I+EAlCNUzRDYDdFwFYUKvXcIA==} + engines: {node: '>=0.10.0'} + dev: true + /@discordjs/builders@1.6.3: resolution: {integrity: sha512-CTCh8NqED3iecTNuiz49mwSsrc2iQb4d0MjMdmS/8pb69Y4IlzJ/DIy/p5GFlgOrFbNO2WzMHkWKQSiJ3VNXaw==} engines: {node: '>=16.9.0'} @@ -50,10 +61,10 @@ packages: '@discordjs/formatters': 0.3.1 '@discordjs/util': 0.3.1 '@sapphire/shapeshift': 3.9.2 - discord-api-types: 0.37.46 + discord-api-types: 0.37.50 fast-deep-equal: 3.1.3 ts-mixer: 6.0.3 - tslib: 2.6.0 + tslib: 2.6.1 dev: false /@discordjs/collection@1.5.1: @@ -65,7 +76,7 @@ packages: resolution: {integrity: sha512-M7X4IGiSeh4znwcRGcs+49B5tBkNDn4k5bmhxJDAUhRxRHTiFAOTVUNQ6yAKySu5jZTnCbSvTYHW3w0rAzV1MA==} engines: {node: '>=16.9.0'} dependencies: - discord-api-types: 0.37.46 + discord-api-types: 0.37.50 dev: false /@discordjs/rest@1.7.1: @@ -76,9 +87,9 @@ packages: '@discordjs/util': 0.3.1 '@sapphire/async-queue': 1.5.0 '@sapphire/snowflake': 3.5.1 - discord-api-types: 0.37.46 + discord-api-types: 0.37.50 file-type: 18.5.0 - tslib: 2.6.0 + tslib: 2.6.1 undici: 5.22.1 dev: false @@ -97,8 +108,8 @@ packages: '@sapphire/async-queue': 1.5.0 '@types/ws': 8.5.5 '@vladfrangu/async_event_emitter': 2.2.2 - discord-api-types: 0.37.46 - tslib: 2.6.0 + discord-api-types: 0.37.50 + tslib: 2.6.1 ws: 8.13.0 transitivePeerDependencies: - bufferutil @@ -334,18 +345,18 @@ packages: eslint-visitor-keys: 3.4.1 dev: true - /@eslint-community/regexpp@4.5.1: - resolution: {integrity: sha512-Z5ba73P98O1KUYCCJTUeVpja9RcGoMdncZ6T49FCUl2lN38JtCJ+3WgIDBv0AuY4WChU5PmtJmOCTlN6FZTFKQ==} + /@eslint-community/regexpp@4.6.1: + resolution: {integrity: sha512-O7x6dMstWLn2ktjcoiNLDkAGG2EjveHL+Vvc+n0fXumkJYAcSqcVYKtwDU+hDZ0uDUsnUagSYaZrOLAYE8un1A==} engines: {node: ^12.0.0 || ^14.0.0 || >=16.0.0} dev: true - /@eslint/eslintrc@2.0.3: - resolution: {integrity: sha512-+5gy6OQfk+xx3q0d6jGZZC3f3KzAkXc/IanVxd1is/VIIziRqqt3ongQz0FiTUXqTk0c7aDB3OaFuKnuSoJicQ==} + /@eslint/eslintrc@2.1.0: + resolution: {integrity: sha512-Lj7DECXqIVCqnqjjHMPna4vn6GJcMgul/wuS0je9OZ9gsL0zzDpKPVtcG1HaDVc+9y+qgXneTeUMbCqXJNpH1A==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: ajv: 6.12.6 debug: 4.3.4 - espree: 9.5.2 + espree: 9.6.1 globals: 13.20.0 ignore: 5.2.4 import-fresh: 3.3.0 @@ -452,7 +463,7 @@ packages: typescript: optional: true dependencies: - '@eslint-community/regexpp': 4.5.1 + '@eslint-community/regexpp': 4.6.1 '@typescript-eslint/parser': 5.60.1(eslint@8.43.0)(typescript@5.1.3) '@typescript-eslint/scope-manager': 5.60.1 '@typescript-eslint/type-utils': 5.60.1(eslint@8.43.0)(typescript@5.1.3) @@ -462,7 +473,7 @@ packages: grapheme-splitter: 1.0.4 ignore: 5.2.4 natural-compare-lite: 1.4.0 - semver: 7.5.3 + semver: 7.5.4 tsutils: 3.21.0(typescript@5.1.3) typescript: 5.1.3 transitivePeerDependencies: @@ -536,7 +547,7 @@ packages: debug: 4.3.4 globby: 11.1.0 is-glob: 4.0.3 - semver: 7.5.3 + semver: 7.5.4 tsutils: 3.21.0(typescript@5.1.3) typescript: 5.1.3 transitivePeerDependencies: @@ -557,7 +568,7 @@ packages: '@typescript-eslint/typescript-estree': 5.60.1(typescript@5.1.3) eslint: 8.43.0 eslint-scope: 5.1.1 - semver: 7.5.3 + semver: 7.5.4 transitivePeerDependencies: - supports-color - typescript @@ -576,16 +587,16 @@ packages: engines: {node: '>=v14.0.0', npm: '>=7.0.0'} dev: false - /acorn-jsx@5.3.2(acorn@8.9.0): + /acorn-jsx@5.3.2(acorn@8.10.0): resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: acorn: ^6.0.0 || ^7.0.0 || ^8.0.0 dependencies: - acorn: 8.9.0 + acorn: 8.10.0 dev: true - /acorn@8.9.0: - resolution: {integrity: sha512-jaVNAFBHNLXspO543WnNNPZFRtavh3skAkITqD0/2aeMkKZTN+254PyhwxFYrk3vQ1xfY+2wbesJMs/JC8/PwQ==} + /acorn@8.10.0: + resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} engines: {node: '>=0.4.0'} hasBin: true dev: true @@ -679,10 +690,22 @@ packages: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} dev: true + /colorette@2.0.20: + resolution: {integrity: sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==} + dev: false + /concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true + /cross-env@7.0.3: + resolution: {integrity: sha512-+/HKd6EgcQCJGh2PSjZuUitQBQynKor4wrFbRg4DtAgS1aWO+gU52xpH7M9ScGgXSYmAVS9bIJ8EzuaGw0oNAw==} + engines: {node: '>=10.14', npm: '>=6', yarn: '>=1'} + hasBin: true + dependencies: + cross-spawn: 7.0.3 + dev: true + /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -715,8 +738,8 @@ packages: path-type: 4.0.0 dev: true - /discord-api-types@0.37.46: - resolution: {integrity: sha512-DeSi5WSWYTeXJJhdwACtpQycY3g4vLRvE2Ol5IlC0o//P2W+8jXPF447PuJn2fRH1nD7JGEJ3YMb0NB9+OQ7BQ==} + /discord-api-types@0.37.50: + resolution: {integrity: sha512-X4CDiMnDbA3s3RaUXWXmgAIbY1uxab3fqe3qwzg5XutR3wjqi7M3IkgQbsIBzpqBN2YWr/Qdv7JrFRqSgb4TFg==} dev: false /discord.js@14.11.0: @@ -731,10 +754,10 @@ packages: '@discordjs/ws': 0.8.3 '@sapphire/snowflake': 3.5.1 '@types/ws': 8.5.5 - discord-api-types: 0.37.46 + discord-api-types: 0.37.50 fast-deep-equal: 3.1.3 lodash.snakecase: 4.1.1 - tslib: 2.6.0 + tslib: 2.6.1 undici: 5.22.1 ws: 8.13.0 transitivePeerDependencies: @@ -797,8 +820,8 @@ packages: estraverse: 4.3.0 dev: true - /eslint-scope@7.2.0: - resolution: {integrity: sha512-DYj5deGlHBfMt15J7rdtyKNq/Nqlv5KfU4iodrQ019XESsRnwXH9KAE0y3cwtUHDo2ob7CypAnCqefh6vioWRw==} + /eslint-scope@7.2.1: + resolution: {integrity: sha512-CvefSOsDdaYYvxChovdrPo/ZGt8d5lrJWleAc1diXRKhHGiTYEI26cvo8Kle/wGnsizoCJjK73FMg1/IkIwiNA==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: esrecurse: 4.3.0 @@ -816,8 +839,8 @@ packages: hasBin: true dependencies: '@eslint-community/eslint-utils': 4.4.0(eslint@8.43.0) - '@eslint-community/regexpp': 4.5.1 - '@eslint/eslintrc': 2.0.3 + '@eslint-community/regexpp': 4.6.1 + '@eslint/eslintrc': 2.1.0 '@eslint/js': 8.43.0 '@humanwhocodes/config-array': 0.11.10 '@humanwhocodes/module-importer': 1.0.1 @@ -828,9 +851,9 @@ packages: debug: 4.3.4 doctrine: 3.0.0 escape-string-regexp: 4.0.0 - eslint-scope: 7.2.0 + eslint-scope: 7.2.1 eslint-visitor-keys: 3.4.1 - espree: 9.5.2 + espree: 9.6.1 esquery: 1.5.0 esutils: 2.0.3 fast-deep-equal: 3.1.3 @@ -850,7 +873,7 @@ packages: lodash.merge: 4.6.2 minimatch: 3.1.2 natural-compare: 1.4.0 - optionator: 0.9.1 + optionator: 0.9.3 strip-ansi: 6.0.1 strip-json-comments: 3.1.1 text-table: 0.2.0 @@ -858,12 +881,12 @@ packages: - supports-color dev: true - /espree@9.5.2: - resolution: {integrity: sha512-7OASN1Wma5fum5SrNhFMAMJxOUAbhyfQ8dQ//PJaJbNw0URTPWqIghHWt1MmAANKhHZIYOHruW4Kw4ruUWOdGw==} + /espree@9.6.1: + resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} dependencies: - acorn: 8.9.0 - acorn-jsx: 5.3.2(acorn@8.9.0) + acorn: 8.10.0 + acorn-jsx: 5.3.2(acorn@8.10.0) eslint-visitor-keys: 3.4.1 dev: true @@ -912,8 +935,8 @@ packages: /fast-deep-equal@3.1.3: resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} - /fast-glob@3.2.12: - resolution: {integrity: sha512-DVj4CQIYYow0BlaelwK1pHl5n5cRSJfM60UA0zK891sVInoPri2Ekj7+e1CT3/3qxXenpI+nBBmQAcJPJgaj4w==} + /fast-glob@3.3.1: + resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} dependencies: '@nodelib/fs.stat': 2.0.5 @@ -1036,7 +1059,7 @@ packages: dependencies: array-union: 2.1.0 dir-glob: 3.0.1 - fast-glob: 3.2.12 + fast-glob: 3.3.1 ignore: 5.2.4 merge2: 1.4.1 slash: 3.0.0 @@ -1227,16 +1250,16 @@ packages: wrappy: 1.0.2 dev: true - /optionator@0.9.1: - resolution: {integrity: sha512-74RlY5FCnhq4jRxVUPKDaRwrVNXMqsGsiW6AJw4XK8hmtm10wC0ypZBLw5IIp85NZMr91+qd1RvvENwg7jjRFw==} + /optionator@0.9.3: + resolution: {integrity: sha512-JjCoypp+jKn1ttEFExxhetCKeJt9zhAgAve5FXHixTvFDW/5aEktX9bufBKLRRMdU7bNtpLfcGu94B3cdEJgjg==} engines: {node: '>= 0.8.0'} dependencies: + '@aashutoshrathi/word-wrap': 1.2.6 deep-is: 0.1.4 fast-levenshtein: 2.0.6 levn: 0.4.1 prelude-ls: 1.2.1 type-check: 0.4.0 - word-wrap: 1.2.3 dev: true /p-limit@3.1.0: @@ -1365,8 +1388,8 @@ packages: kind-of: 6.0.3 dev: true - /semver@7.5.3: - resolution: {integrity: sha512-QBlUtyVk/5EeHbi7X0fw6liDZc7BBmEaSYn01fMU1OUYbf6GPsbTtd8WmnqbI20SeycoHSeiybkE/q1Q+qlThQ==} + /semver@7.5.4: + resolution: {integrity: sha512-1bCSESV6Pv+i21Hvpxp3Dx+pSD8lIPt8uVjRrxAUt/nbswYc+tK6Y2btiULjd4+fnq15PX+nqQDC7Oft7WkwcA==} engines: {node: '>=10'} hasBin: true dependencies: @@ -1476,8 +1499,8 @@ packages: resolution: {integrity: sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==} dev: true - /tslib@2.6.0: - resolution: {integrity: sha512-7At1WUettjcSRHXCyYtTselblcHl9PJFFVKiCAy/bY97+BPZXSQ2wbq0P9s8tK2G7dFQfNnlJnPAiArVBVBsfA==} + /tslib@2.6.1: + resolution: {integrity: sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==} dev: false /tsutils@3.21.0(typescript@5.1.3): @@ -1544,11 +1567,6 @@ packages: isexe: 2.0.0 dev: true - /word-wrap@1.2.3: - resolution: {integrity: sha512-Hz/mrNwitNRh/HUAtM/VT/5VH+ygD6DV7mYKZAtHOrbs8U7lvPS6xf7EJKMF0uW1KJCl0H701g3ZGus+muE5vQ==} - engines: {node: '>=0.10.0'} - dev: true - /wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true diff --git a/src/_reupload.ts b/src/_reupload.ts index d49a3df..b215f8e 100644 --- a/src/_reupload.ts +++ b/src/_reupload.ts @@ -1,57 +1,3 @@ -import { - SlashCommandBuilder, - Routes, - PermissionFlagsBits, - type RESTGetAPIOAuth2CurrentApplicationResult, -} from 'discord.js'; -import { REST } from '@discordjs/rest'; -import { getTags } from './tags'; +import { reloadGlobalSlashCommands } from "./handlers/command.handler"; -export const reuploadCommands = async () => { - const tags = await getTags(); - - const commands = [ - new SlashCommandBuilder() - .setName('ping') - .setDescription('Replies with pong!'), - new SlashCommandBuilder() - .setName('tag') - .setDescription('Send a tag') - .addStringOption((option) => - option - .setName('name') - .setDescription('The tag name') - .setRequired(true) - .addChoices(...tags.map((b) => ({ name: b.name, value: b.name }))) - ) - .addUserOption((option) => - option - .setName('user') - .setDescription('The user to mention') - .setRequired(false) - ), - new SlashCommandBuilder() - .setName('say') - .setDescription('Say something through the bot') - .addStringOption((option) => - option - .setName('content') - .setDescription('Just content?') - .setRequired(true) - ) - .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) - .setDMPermission(false), - ].map((command) => command.toJSON()); - - const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN!); - - const { id: appId } = (await rest.get( - Routes.oauth2CurrentApplication() - )) as RESTGetAPIOAuth2CurrentApplicationResult; - - await rest.put(Routes.applicationCommands(appId), { - body: commands, - }); - - console.log('Successfully registered application commands.'); -}; +reloadGlobalSlashCommands() \ No newline at end of file diff --git a/src/commands/_commands.ts b/src/commands/_commands.ts new file mode 100644 index 0000000..54d4ac1 --- /dev/null +++ b/src/commands/_commands.ts @@ -0,0 +1,10 @@ +import { Command } from "../handlers/command.handler" +import { sayCommand } from "./say.command" +import { tagCommand } from "./tag.command" + +export const commands: Command[] = [ + sayCommand, + tagCommand +] + +export default commands \ No newline at end of file diff --git a/src/commands/say.command.ts b/src/commands/say.command.ts new file mode 100644 index 0000000..b4ddd45 --- /dev/null +++ b/src/commands/say.command.ts @@ -0,0 +1,50 @@ +import { + EmbedBuilder, + PermissionFlagsBits, + SlashCommandBuilder, +} from 'discord.js'; +import { Command } from '../handlers/command.handler'; + +export const sayCommand: Command = { + data: new SlashCommandBuilder() + .setName('say') + .setDescription('Say something through the bot') + .addStringOption(option => + option + .setName('content') + .setDescription('Just content?') + .setRequired(true) + ) + .setDefaultMemberPermissions(PermissionFlagsBits.ModerateMembers) + .setDMPermission(false), + async execute(interaction) { + if (!interaction.guild || !interaction.channel) return; + + + const content = interaction.options.get('content', true).value as string + await interaction.deferReply({ ephemeral: true }); + const message = await interaction.channel.send(content); + await interaction.editReply('I said what you said!'); + + if (process.env.SAY_LOGS_CHANNEL) { + const logsChannel = await interaction.guild.channels.fetch( + process.env.SAY_LOGS_CHANNEL + ); + + if (!logsChannel?.isTextBased()) return; + + await logsChannel.send({ + embeds: [ + new EmbedBuilder() + .setTitle('Say command used') + .setDescription(content) + .setAuthor({ + name: interaction.user.tag, + iconURL: interaction.user.avatarURL() ?? undefined, + }) + .setURL(message.url), + ], + }); + } + } +} diff --git a/src/commands/say.ts b/src/commands/say.ts deleted file mode 100644 index fd3af32..0000000 --- a/src/commands/say.ts +++ /dev/null @@ -1,37 +0,0 @@ -import { - CacheType, - ChatInputCommandInteraction, - EmbedBuilder, -} from 'discord.js'; - -export const sayCommand = async ( - interaction: ChatInputCommandInteraction -) => { - if (!interaction.guild || !interaction.channel) return; - - const content = interaction.options.getString('content', true); - await interaction.deferReply({ ephemeral: true }); - const message = await interaction.channel.send(content); - await interaction.editReply('I said what you said!'); - - if (process.env.SAY_LOGS_CHANNEL) { - const logsChannel = await interaction.guild.channels.fetch( - process.env.SAY_LOGS_CHANNEL - ); - - if (!logsChannel?.isTextBased()) return; - - await logsChannel.send({ - embeds: [ - new EmbedBuilder() - .setTitle('Say command used') - .setDescription(content) - .setAuthor({ - name: interaction.user.tag, - iconURL: interaction.user.avatarURL() ?? undefined, - }) - .setURL(message.url), - ], - }); - } -}; diff --git a/src/commands/tag.command.ts b/src/commands/tag.command.ts new file mode 100644 index 0000000..209a601 --- /dev/null +++ b/src/commands/tag.command.ts @@ -0,0 +1,57 @@ +import { + EmbedBuilder, + SlashCommandBuilder, +} from 'discord.js'; +import { getTags, getTagsSync } from '../handlers/tag.handler'; +import { Command } from '../handlers/command.handler'; + +const tags = getTagsSync() + +export const tagCommand: Command = { + // @ts-expect-error idk why it gives an error + data: new SlashCommandBuilder() + .setName('tag') + .setDescription('Send a tag') + .addStringOption((option) => + option + .setName('name') + .setDescription('The tag name') + .setRequired(true) + .addChoices(...tags.map((b) => ({ name: b.name, value: b.name }))) + ) + .addUserOption((option) => + option + .setName('user') + .setDescription('The user to mention') + .setRequired(false) + ), + async execute(i) { + const tags = await getTags(); + const tagName = i.options.get('name', true).value as string; + const mention = i.options.getUser('user', false); + + const tag = tags.find( + (tag) => tag.name === tagName || tag.aliases?.includes(tagName) + ); + + if (!tag) { + await i.reply({ + content: `Tag \`${tagName}\` does not exist.`, + ephemeral: true, + }); + return; + } + + const embed = new EmbedBuilder(); + embed.setTitle(tag.title ?? tag.name); + embed.setDescription(tag.content); + if (tag.color) embed.setColor(tag.color); + if (tag.image) embed.setImage(tag.image); + if (tag.fields) embed.setFields(tag.fields); + + await i.reply({ + content: mention ? `<@${mention.id}> ` : undefined, + embeds: [embed], + }); + }, +} diff --git a/src/commands/tags.ts b/src/commands/tags.ts deleted file mode 100644 index 870e452..0000000 --- a/src/commands/tags.ts +++ /dev/null @@ -1,38 +0,0 @@ -import { - type ChatInputCommandInteraction, - type CacheType, - EmbedBuilder, -} from 'discord.js'; -import { getTags } from '../tags'; - -export const tagsCommand = async ( - i: ChatInputCommandInteraction -) => { - const tags = await getTags(); - const tagName = i.options.getString('name', true); - const mention = i.options.getUser('user', false); - - const tag = tags.find( - (tag) => tag.name === tagName || tag.aliases?.includes(tagName) - ); - - if (!tag) { - await i.reply({ - content: `Tag \`${tagName}\` does not exist.`, - ephemeral: true, - }); - return; - } - - const embed = new EmbedBuilder(); - embed.setTitle(tag.title ?? tag.name); - embed.setDescription(tag.content); - if (tag.color) embed.setColor(tag.color); - if (tag.image) embed.setImage(tag.image); - if (tag.fields) embed.setFields(tag.fields); - - await i.reply({ - content: mention ? `<@${mention.id}> ` : undefined, - embeds: [embed], - }); -}; diff --git a/src/constants.ts b/src/constants.ts deleted file mode 100644 index 8308bf2..0000000 --- a/src/constants.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const COLORS = { - red: 0xef4444, - green: 0x22c55e, - blue: 0x60a5fa, - yellow: 0xfde047, - pink: 0xffc0cb, - orange: 0xfb923c, -} as { [key: string]: number }; diff --git a/src/handlers/command.handler.ts b/src/handlers/command.handler.ts new file mode 100644 index 0000000..8a3d058 --- /dev/null +++ b/src/handlers/command.handler.ts @@ -0,0 +1,80 @@ +import { Handler } from ".."; +import dotenv from 'dotenv' +import { REST, Routes, SlashCommandBuilder, CommandInteraction, Collection, EmbedBuilder, RESTGetAPIOAuth2CurrentApplicationResult } from "discord.js"; +import * as color from 'colorette' +import commands from "../commands/_commands"; + +dotenv.config() + + +const rest = new REST({ version: '10' }).setToken(process.env.DISCORD_TOKEN); + +// LOAD COMMANDS AND UPLOAD TO DISCORD // + +export interface Command { // create an interface accessible from another files used to create commands + data: SlashCommandBuilder, + execute: (interaction: CommandInteraction) => unknown +} + +const commandCollection = new Collection(commands.map(command => [command.data.name, command])) + +console.log() // prints an empty line + +console.log(color.blueBright(`Loaded ${commands.length} command${commands.length == 1 ? "" : "s"}:`)) + +for (const index in commands) { + const command = commands[index] + console.log(`|| ${color.whiteBright(parseInt(index) + 1 + ". " + command.data.name)}:\n|| → ${command.data.description}`) +} + + +console.log() // prints an empty line + +// taken from discord.js docs and transformed a bit +export async function reloadGlobalSlashCommands() { + try { + console.log(color.bgYellowBright(`Started refreshing ${commands.length} application (/) commands.`)); + console.time(color.yellowBright("Reloading global commands")) + + // The put method is used to fully refresh all commands with the current set + + const { id: appId } = (await rest.get( + Routes.oauth2CurrentApplication() + )) as RESTGetAPIOAuth2CurrentApplicationResult; + + await rest.put( + Routes.applicationCommands(appId), + { body: commands.map(commandList => commandList.data.toJSON()) }, + ); + + console.log(`Successfully reloaded global application (/) commands.`); + console.timeEnd(color.yellowBright("Reloading global commands")) + } catch (error) { + // And of course, make sure you catch and log any errors! + console.error(error); + } +} + + +const commandHandler: Handler = (client) => { + client.on('interactionCreate', async interaction => { + if (!interaction.isCommand()) return // make sure that the interaction came from a command + if (!commandCollection.has(interaction.commandName)) return // and that the command exist on this app + + try { + await commandCollection.get(interaction.commandName)!.execute(interaction) // try execute the command + } catch (error) { // in case of an error + await interaction.followUp({ // send a followup to the interaction + embeds: [ + new EmbedBuilder({ + color: 0xff2222, + title: "Internal error" + }) + ] + }) + return; + } + }) +} + +export default commandHandler \ No newline at end of file diff --git a/src/handlers/log.handler.ts b/src/handlers/log.handler.ts new file mode 100644 index 0000000..c08bc57 --- /dev/null +++ b/src/handlers/log.handler.ts @@ -0,0 +1,90 @@ +import { EmbedBuilder, Events } from 'discord.js'; + +// log providers +import logProviders from '../logProviders/_logProviders' +import logAnalyzers from '../logAnalyzers/_logAnalyzers'; + +import { Handler } from '..'; + +export type LogAnalyzer = (url: string) => Promise; +export interface LogProvider { + hostnames: string[] + parse: (url: string) => Promise +} + +const hostnameMap = new Map Promise>() + +for (const provider of logProviders) { + provider.hostnames.forEach(hostname => hostnameMap.set(hostname, provider.parse)) +} + +async function parseWebLog(text: string): Promise { + const reg = text.match(/https?:\/\/(www\.)?[-a-zA-Z0-9@:%._+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b([-a-zA-Z0-9()@:%_+.~#?&//=]*)/) + if (!reg) return + const url = reg[0] + const hostname = url.split("/")[2] + if (!hostnameMap.has(hostname)) return; + return hostnameMap.get(hostname)!(url) +} + +async function findIssues(log: string) { + const issues: { name: string, value: string }[] = [] + + for (const analyzer of logAnalyzers) { + const issue = await analyzer(log) + if (issue) issues.push(issue) + } + + return issues +} + + + +export const logHandler: Handler = (client) => { + client.on(Events.MessageCreate, async (message) => { + try { + if (message.channel.partial) await message.channel.fetch(); + if (message.author.partial) await message.author.fetch(); + + const attachment = message.attachments.find(attachment => attachment.contentType == "text/plain; charset=utf-8") + + if (!message.content && !attachment) return; + if (!message.channel.isTextBased()) return; + + if (message.author === client.user) return; + + const log = attachment + ? await (await fetch(attachment.url)).text() + : await parseWebLog(message.content); + + + if (!log) return + + const regexPasses = [ + /---- Minecraft Crash Report ----/, // Forge Crash report + /\n\\|[\\s\\d]+\\| Minecraft\\s+\\| minecraft\\s+\\| (\\S+).+\n/, // Quilt mod table + /: Loading Minecraft (\\S+)/, // Fabric, Quilt + /--fml.mcVersion, ([^\\s,]+)/, // Forge + /--version, ([^,]+),/, // ATLauncher + / --version (\\S+) / // MMC, Prism, PolyMC + ] + + if (!regexPasses.find(reg => log.match(reg))) return + + const issues = await findIssues(log) + + const embed = new EmbedBuilder() + .setTitle("Log analysis") + .setDescription(`${issues.length || "No"} issue${issues.length == 1 ? "" : "s"} found automatically`) + .setFields(...issues) + .setColor(issues.length ? "Red" : "Green") + + if (log != null) { + message.reply({ embeds: [embed] }); + return; + } + } catch (error) { + console.error('Unhandled exception on MessageCreate', error); + } + }); +} diff --git a/src/tags.ts b/src/handlers/tag.handler.ts similarity index 60% rename from src/tags.ts rename to src/handlers/tag.handler.ts index 3df1e2e..14125b2 100644 --- a/src/tags.ts +++ b/src/handlers/tag.handler.ts @@ -1,9 +1,9 @@ import matter from 'gray-matter'; import { readdir, readFile } from 'fs/promises'; import { join } from 'path'; -import { COLORS } from './constants'; import { type EmbedField } from 'discord.js'; +import { readdirSync, readFileSync } from 'fs'; interface Tag { name: string; @@ -29,9 +29,28 @@ export const getTags = async (): Promise => { ...data, name: _file.replace('.md', ''), content: content.trim(), - color: data.color ? COLORS[data.color] : undefined, + color: data.color || undefined, }); } return tags; }; + +export const getTagsSync = (): Tag[] => { + const filenames = readdirSync(TAG_DIR) + const tags: Tag[] = [] + + for (const _file of filenames) { + const file = join(TAG_DIR, _file); + const { data, content } = matter(readFileSync(file)); + + tags.push({ + ...data, + name: _file.replace('.md', ''), + content: content.trim(), + color: data.color || undefined, + }); + } + + return tags; +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts index 7f57123..1a5de71 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,12 +1,8 @@ -import { Client, GatewayIntentBits, Partials, Events } from 'discord.js'; -import { reuploadCommands } from './_reupload'; - -import { parseLog } from './logs'; - -import { tagsCommand } from './commands/tags'; -import { sayCommand } from './commands/say'; +import { Client, GatewayIntentBits, Partials } from 'discord.js'; import 'dotenv/config'; +import commandHandler from './handlers/command.handler'; +import { logHandler } from './handlers/log.handler'; const client = new Client({ intents: [ @@ -40,54 +36,18 @@ client.once('ready', async () => { activities: [{ name: `Steam 'n' Rails` }], status: 'online', }); - - client.on(Events.MessageCreate, async (e) => { - try { - if (e.channel.partial) await e.channel.fetch(); - if (e.author.partial) await e.author.fetch(); - - if (!e.content) return; - if (!e.channel.isTextBased()) return; - - if (e.author === client.user) return; - - const log = await parseLog(e.content); - if (log != null) { - e.reply({ embeds: [log] }); - return; - } - } catch (error) { - console.error('Unhandled exception on MessageCreate', error); - } - }); }); -client.on(Events.InteractionCreate, async (interaction) => { - try { - if (!interaction.isChatInputCommand()) return; +export type Handler = (client: Client) => void; - const { commandName } = interaction; - if (commandName === 'ping') { - await interaction.reply({ - content: `Pong! \`${client.ws.ping}ms\``, - ephemeral: true, - }); - } else if (commandName === 'say') { - await sayCommand(interaction); - } else if (commandName === 'tag') { - await tagsCommand(interaction); - } - } catch (error) { - console.error('Unhandled exception on InteractionCreate', error); - } -}); +const handlers: Handler[] = [ + commandHandler, + logHandler +] -reuploadCommands() - .then(() => { - client.login(process.env.DISCORD_TOKEN); - }) - .catch((e) => { - console.error(e); - process.exit(1); - }); +for (const handler of handlers) { + handler(client) +} + +client.login(process.env.DISCORD_TOKEN) \ No newline at end of file diff --git a/src/logAnalyzers/_logAnalyzers.ts b/src/logAnalyzers/_logAnalyzers.ts new file mode 100644 index 0000000..5d0456e --- /dev/null +++ b/src/logAnalyzers/_logAnalyzers.ts @@ -0,0 +1,9 @@ +import { LogAnalyzer } from '../handlers/log.handler' +import { optifineAnalyzer } from './optifine' + + +export const logAnalyzers: LogAnalyzer[] = [ + optifineAnalyzer +] + +export default logAnalyzers \ No newline at end of file diff --git a/src/logAnalyzers/optifine.ts b/src/logAnalyzers/optifine.ts new file mode 100644 index 0000000..68578ae --- /dev/null +++ b/src/logAnalyzers/optifine.ts @@ -0,0 +1,13 @@ +import { LogAnalyzer } from "../handlers/log.handler"; + +export const optifineAnalyzer: LogAnalyzer = async (text) => { + const matchesOptifine = text.match(/f_174747_/); + if (matchesOptifine) { + return { + name: 'Incompatible with OptiFine', + value: "OptiFine breaks Steam 'n' Rails and is Incompatible\n\nCheck `/tag optifine` for more info & alternatives you can use.", + }; + } + return null; +}; + diff --git a/src/logproviders/0x0.ts b/src/logproviders/0x0.ts index 65774e6..f4a3656 100644 --- a/src/logproviders/0x0.ts +++ b/src/logproviders/0x0.ts @@ -1,19 +1,24 @@ +import { LogProvider } from "../handlers/log.handler"; + const reg = /https:\/\/0x0.st\/\w*.\w*/; -export async function read0x0(s: string): Promise { - const r = s.match(reg); - if (r == null || !r[0]) return null; - const link = r[0]; - let log: string; - try { - const f = await fetch(link); - if (f.status != 200) { - throw 'nope'; +export const r0x0: LogProvider = { + hostnames: ["0x0.st"], + async parse(text) { + const r = text.match(reg); + if (r == null || !r[0]) return; + const link = r[0]; + let log: string; + try { + const f = await fetch(link); + if (f.status != 200) { + throw 'nope'; + } + log = await f.text(); + } catch (err) { + console.log('Log analyze fail', err); + return; } - log = await f.text(); - } catch (err) { - console.log('Log analyze fail', err); - return null; - } - return log; -} + return log; + }, +} \ No newline at end of file diff --git a/src/logproviders/_logProviders.ts b/src/logproviders/_logProviders.ts new file mode 100644 index 0000000..81c174f --- /dev/null +++ b/src/logproviders/_logProviders.ts @@ -0,0 +1,16 @@ +import { LogProvider } from '../handlers/log.handler' +import { mcLoGs } from './mclogs'; +import { r0x0 } from './0x0'; +import { pasteGG } from './pastegg'; +import { hastebin } from './haste'; +import { pastebin } from './pastebin'; + +export const logProviders: LogProvider[] = [ + mcLoGs, + r0x0, + pasteGG, + hastebin, + pastebin +] + +export default logProviders \ No newline at end of file diff --git a/src/logproviders/haste.ts b/src/logproviders/haste.ts index c295f41..af107b5 100644 --- a/src/logproviders/haste.ts +++ b/src/logproviders/haste.ts @@ -1,21 +1,28 @@ +import { LogProvider } from "../handlers/log.handler"; + const reg = /https:\/\/hst.sh\/[\w]*/; -export async function readHastebin(s: string): Promise { - const r = s.match(reg); - if (r == null || !r[0]) return null; - const link = r[0]; - const id = link.replace('https://hst.sh/', ''); - if (!id) return null; - let log: string; - try { - const f = await fetch(`https://hst.sh/raw/${id}`); - if (f.status != 200) { - throw 'nope'; + + +export const hastebin: LogProvider = { + hostnames: ["hst.sh"], + async parse(text) { + const r = text.match(reg); + if (r == null || !r[0]) return; + const link = r[0]; + const id = link.replace('https://hst.sh/', ''); + if (!id) return; + let log: string; + try { + const f = await fetch(`https://hst.sh/raw/${id}`); + if (f.status != 200) { + throw 'nope'; + } + log = await f.text(); + } catch (err) { + console.log('Log analyze fail', err); + return; } - log = await f.text(); - } catch (err) { - console.log('Log analyze fail', err); - return null; - } - return log; + return log; + }, } diff --git a/src/logproviders/mclogs.ts b/src/logproviders/mclogs.ts index eecda09..8f34a01 100644 --- a/src/logproviders/mclogs.ts +++ b/src/logproviders/mclogs.ts @@ -1,22 +1,28 @@ +import { LogProvider } from "../handlers/log.handler"; + const reg = /https:\/\/mclo.gs\/\w*/; -export async function readMcLogs(s: string): Promise { - const r = s.match(reg); - if (r == null || !r[0]) return null; - const link = r[0]; - const id = link.replace('https://mclo.gs/', ''); - if (!id) return null; - const apiUrl = 'https://api.mclo.gs/1/raw/' + id; - let log: string; - try { - const f = await fetch(apiUrl); - if (f.status != 200) { - throw 'nope'; + +export const mcLoGs: LogProvider = { + hostnames: ["mclo.gs"], + async parse(text) { + const r = text.match(reg); + if (r == null || !r[0]) return; + const link = r[0]; + const id = link.replace('https://mclo.gs/', ''); + if (!id) return; + const apiUrl = 'https://api.mclo.gs/1/raw/' + id; + let log: string; + try { + const res = await fetch(apiUrl); + if (res.status != 200) { + throw 'nope'; + } + log = await res.text(); + } catch (err) { + console.log('Log analyze fail', err); + return; } - log = await f.text(); - } catch (err) { - console.log('Log analyze fail', err); - return null; - } - return log; -} + return log; + }, +} \ No newline at end of file diff --git a/src/logproviders/pastebin.ts b/src/logproviders/pastebin.ts new file mode 100644 index 0000000..6d08784 --- /dev/null +++ b/src/logproviders/pastebin.ts @@ -0,0 +1,13 @@ +import { LogProvider } from "../handlers/log.handler"; + +export const pastebin: LogProvider = { + hostnames: ["pastebin.com"], + async parse(url) { + const id = url.slice(-8) + console.log(id) + console.log(`https://pastebin.com/raw/${id}`) + const res = await fetch(`https://pastebin.com/raw/${id}`) + if (res.status !== 200) return + return res.text() + }, +} diff --git a/src/logproviders/pastegg.ts b/src/logproviders/pastegg.ts index 11181e7..1c625d3 100644 --- a/src/logproviders/pastegg.ts +++ b/src/logproviders/pastegg.ts @@ -1,30 +1,33 @@ +import { LogProvider } from "../handlers/log.handler"; + const reg = /https:\/\/paste.gg\/p\/[\w]*\/[\w]*/; -export async function readPasteGG(s: string): Promise { - const r = s.match(reg); - if (r == null || !r[0]) return null; - const link = r[0]; - const id = link.replace(/https:\/\/paste.gg\/p\/[\w]*\//, ''); - if (!id) return null; - let log: string; - try { - const pasteJson = await ( - await fetch('https://api.paste.gg/v1/pastes/' + id) - ).json(); - if (pasteJson.status != 'success') throw 'up'; - const pasteData = await ( - await fetch( - 'https://api.paste.gg/v1/pastes/' + +export const pasteGG: LogProvider = { + hostnames: ["paste.gg"], + async parse(text) { + const r = text.match(reg); + if (r == null || !r[0]) return null; + const link = r[0]; + const id = link.replace(/https:\/\/paste.gg\/p\/[\w]*\//, ''); + if (!id) return null; + try { + const pasteJson = await ( + await fetch('https://api.paste.gg/v1/pastes/' + id) + ).json(); + if (pasteJson.status != 'success') throw 'up'; + const pasteData = await ( + await fetch( + 'https://api.paste.gg/v1/pastes/' + id + '/files/' + pasteJson.result.files[0].id - ) - ).json(); - if (pasteData.status != 'success') throw 'up'; - return pasteData.result.content.value; - } catch (err) { - console.log('Log analyze fail', err); - return null; - } - return log; + ) + ).json(); + if (pasteData.status != 'success') throw 'up'; + return pasteData.result.content.value; + } catch (err) { + console.log('Log analyze fail', err); + return null; + } + }, } diff --git a/src/logs.ts b/src/logs.ts deleted file mode 100644 index ffc9449..0000000 --- a/src/logs.ts +++ /dev/null @@ -1,78 +0,0 @@ -import { EmbedBuilder } from 'discord.js'; - -// log providers -import { readMcLogs } from './logproviders/mclogs'; -import { read0x0 } from './logproviders/0x0'; -import { readPasteGG } from './logproviders/pastegg'; -import { readHastebin } from './logproviders/haste'; -import { COLORS } from './constants'; - -type Analyzer = (text: string) => Promise | null>; -type LogProvider = (text: string) => Promise; - -const optifineAnalyzer: Analyzer = async (text) => { - const matchesOptifine = text.match(/f_174747_/); - if (matchesOptifine) { - return [ - 'Incompatible with OptiFine', - "OptiFine breaks Steam 'n' Rails and is Incompatible\n\nCheck `/tag optifine` for more info & alternatives you can use.", - ]; - } - return null; -}; - -const analyzers: Analyzer[] = [optifineAnalyzer]; - -const providers: LogProvider[] = [ - readMcLogs, - read0x0, - readPasteGG, - readHastebin, -]; - -export async function parseLog(s: string): Promise { - if (/(https?:\/\/)?pastebin\.com\/(raw\/)?[^/\s]{8}/g.test(s)) { - const embed = new EmbedBuilder() - .setTitle('pastebin.com detected') - .setDescription('Please use https://mclo.gs or another paste provider') - .setColor(COLORS.red); - return embed; - } - - let log = ''; - for (const i in providers) { - const provider = providers[i]; - const res = await provider(s); - if (res) { - log = res; - break; - } else { - continue; - } - } - if (!log) return null; - const embed = new EmbedBuilder().setTitle('Log analysis'); - - let thereWasAnIssue = false; - for (const i in analyzers) { - const Analyzer = analyzers[i]; - const out = await Analyzer(log); - if (out) { - embed.addFields({ name: out[0], value: out[1] }); - thereWasAnIssue = true; - } - } - - if (thereWasAnIssue) { - embed.setColor(COLORS.red); - return embed; - } else { - embed.setColor(COLORS.green); - embed.addFields({ - name: 'Analyze failed', - value: 'No issues found automatically', - }); - - return embed; - } -} diff --git a/src/types/environment.d.ts b/src/types/environment.d.ts new file mode 100644 index 0000000..a2dc756 --- /dev/null +++ b/src/types/environment.d.ts @@ -0,0 +1,11 @@ +export { }; + +declare global { + namespace NodeJS { + interface ProcessEnv { + DISCORD_TOKEN: string; + SAY_LOGS_CHANNEL: string; + LOGS_CHANNEL: string; + } + } +} diff --git a/tags/binary-search.md b/tags/binary-search.md index 5472343..484fd76 100644 --- a/tags/binary-search.md +++ b/tags/binary-search.md @@ -1,6 +1,6 @@ --- title: Binary Search - A method of finding problems with mods -color: blue +color: Blue aliases: ['thanosmethod'] --- diff --git a/tags/log.md b/tags/log.md index 0f45f86..686c613 100644 --- a/tags/log.md +++ b/tags/log.md @@ -1,6 +1,6 @@ --- title: Upload Logs -color: orange +color: Orange aliases: ['sendlog', 'logs', '🪵'] --- diff --git a/tags/modernfix.md b/tags/modernfix.md index 1713cb4..b3e8dca 100644 --- a/tags/modernfix.md +++ b/tags/modernfix.md @@ -1,6 +1,6 @@ --- title: Fixing modernfix compatibility issue -color: pink +color: Pink aliases: ['modernfixcompat'] --- diff --git a/tags/optifine.md b/tags/optifine.md index bf53212..a0c11f9 100644 --- a/tags/optifine.md +++ b/tags/optifine.md @@ -1,6 +1,6 @@ --- title: OptiFine -color: green +color: Green aliases: ['of', 'optimize', 'opticrap', 'notfine'] ---