diff --git a/package.json b/package.json index 924c6ca4..49afb173 100644 --- a/package.json +++ b/package.json @@ -16,6 +16,8 @@ }, "license": "MIT", "dependencies": { + "@discordjs/builders": "^0.11.0", + "@discordjs/rest": "^0.2.0-canary.0", "date-fns": "^2.27.0", "discord.js": "^13.5.0", "dotenv": "^10.0.0", @@ -28,6 +30,7 @@ "@types/node-fetch": "^2.5.4", "@typescript-eslint/eslint-plugin": "^5.9.0", "@typescript-eslint/parser": "^5.9.0", + "discord-api-types": "^0.26.1", "eslint": "^8.6.0", "eslint-config-prettier": "^8.3.0", "prettier": "^2.3.2", diff --git a/src/features/commands.ts b/src/features/commands.ts index 42fedcbc..089e69d7 100644 --- a/src/features/commands.ts +++ b/src/features/commands.ts @@ -1,6 +1,5 @@ /* eslint-disable @typescript-eslint/no-use-before-define */ -import fetch from "node-fetch"; -import { Message, TextChannel } from "discord.js"; +import { CommandInteraction, GuildMember, Message } from "discord.js"; import cooldown from "./cooldown"; import { ChannelHandlers } from "../types"; import { isStaff } from "../helpers/discord"; @@ -13,7 +12,7 @@ type Command = { words: string[]; help: string; category: Categories; - handleMessage: (msg: Message) => void; + handleMessage: (msg: Message | CommandInteraction) => void; cooldown?: number; }; @@ -24,9 +23,21 @@ const sortedCategories: Categories[] = [ "React/Redux", ]; -const commandsList: Command[] = [ +async function isStaffMsg(msg: Message | CommandInteraction) { + return Boolean( + msg.guild && + msg.member && + isStaff( + msg.member instanceof GuildMember + ? msg.member + : await msg.guild.members.fetch(msg.member.user.id), + ), + ); +} + +export const commandsList: Command[] = [ { - words: [`!commands`], + words: [`commands`], help: `lists all available commands`, category: "Reactiflux", handleMessage: (msg) => { @@ -45,11 +56,11 @@ const commandsList: Command[] = [ }, }, { - words: [`!rrlinks`], + words: [`rrlinks`], help: `shares a repository of helpful links regarding React and Redux`, category: "React/Redux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "Helpful links", @@ -62,11 +73,11 @@ const commandsList: Command[] = [ }, }, { - words: [`!xy`], + words: [`xy`], help: `explains the XY problem`, category: "Communication", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "The XY Issue", @@ -79,11 +90,11 @@ const commandsList: Command[] = [ }, }, { - words: [`!ymnnr`], + words: [`ymnnr`], help: `links to the You Might Not Need Redux article`, category: "React/Redux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "You Might Not Need Redux", @@ -98,11 +109,11 @@ https://medium.com/@dan_abramov/you-might-not-need-redux-be46360cf367`, }, }, { - words: [`!derived`], + words: [`derived`], help: `Links to the React docs advice to avoid copying props to state`, category: "React/Redux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: @@ -118,11 +129,11 @@ https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html`, }, }, { - words: [`!su`, `!stateupdates`], + words: [`su`, `stateupdates`], help: `Explains the implications involved with state updates being asynchronous`, category: "React/Redux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "State Updates May Be Asynchronous", @@ -147,11 +158,11 @@ https://blog.isquaredsoftware.com/2020/05/blogged-answers-a-mostly-complete-guid }, }, { - words: [`!bind`], + words: [`bind`], help: `explains how and why to bind in React applications`, category: "React/Redux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "Binding functions", @@ -192,11 +203,11 @@ Check out https://reactkungfu.com/2015/07/why-and-how-to-bind-methods-in-your-re }, }, { - words: [`!lift`], + words: [`lift`], help: `links to the React docs regarding the common need to "lift" state`, category: "React/Redux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "Lifting State Up", @@ -212,11 +223,11 @@ https://reactjs.org/docs/lifting-state-up.html`, }, { - words: [`!ask`], + words: [`ask`], help: `explains how to ask questions`, category: "Reactiflux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "Asking to ask", @@ -244,11 +255,11 @@ How To Ask Questions The Smart Way https://git.io/JKscV }, }, { - words: [`!code`, `!gist`], + words: [`code`, `gist`], help: `explains how to attach code`, category: "Reactiflux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "Attaching Code", @@ -270,11 +281,11 @@ Link a Snack to share React Native examples: https://snack.expo.io }, }, { - words: [`!ping`], + words: [`ping`], help: `explains how to ping politely`, category: "Reactiflux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "Don’t ping or DM other devs you aren’t actively talking to", @@ -289,11 +300,11 @@ Similarly, don’t DM other members without asking first. All of the same proble }, }, { - words: [`!inputs`], + words: [`inputs`], help: `provides links to uncontrolled vs controlled components`, category: "React/Redux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "Uncontrolled vs Controlled components", @@ -307,80 +318,80 @@ Here's an article explaining the difference between the two: https://goshakkk.na }); }, }, + // { + // words: [`move`], + // help: `allows you to move the conversation to another channel \n\t(usage: \`!move #toChannel @person1 @person2 @person3\`)`, + // category: "Reactiflux", + // handleMessage: (msg) => { + // const [, newChannel] = msg.content.split(" "); + + // try { + // const targetChannel = msg.guild?.channels.cache.get( + // newChannel.replace("<#", "").replace(">", ""), + // ) as TextChannel; + + // if (!msg.mentions.members) return; + + // targetChannel.send( + // `${msg.author} has opened a portal from ${ + // msg.channel + // } summoning ${msg.mentions.members.map((i) => i).join(" ")}`, + // ); + // } catch (e) { + // console.log("Something went wrong when summoning a portal: ", e); + // } + // }, + // }, + // { + // words: [`mdn`], + // help: `allows you to search something on MDN, usage: !mdn Array.prototype.map`, + // category: "Web", + // handleMessage: async (msg) => { + // const [, ...args] = msg.content.split(" "); + // const query = args.join(" "); + // const [fetchMsg, res] = await Promise.all([ + // msg.channel.send(`Fetching "${query}"...`), + // fetch( + // `https://developer.mozilla.org/api/v1/search/en-US?highlight=false&q=${query}`, + // ), + // ]); + + // const { documents } = await res.json(); + // const [topResult] = documents; + + // if (!topResult) { + // fetchMsg.edit(`Could not find anything on MDN for '${query}'`); + // return; + // } + + // const { title, excerpt: description, mdn_url: mdnUrl } = topResult; + + // await msg.channel?.send({ + // embeds: [ + // { + // author: { + // name: "MDN", + // url: "https://developer.mozilla.org", + // icon_url: + // "https://developer.mozilla.org/static/img/opengraph-logo.72382e605ce3.png", + // }, + // title, + // description, + // color: 0x83d0f2, + // url: `https://developer.mozilla.org${mdnUrl}`, + // }, + // ], + // }); + + // fetchMsg.delete(); + // }, + // }, { - words: [`!move`], - help: `allows you to move the conversation to another channel \n\t(usage: \`!move #toChannel @person1 @person2 @person3\`)`, - category: "Reactiflux", - handleMessage: (msg) => { - const [, newChannel] = msg.content.split(" "); - - try { - const targetChannel = msg.guild?.channels.cache.get( - newChannel.replace("<#", "").replace(">", ""), - ) as TextChannel; - - if (!msg.mentions.members) return; - - targetChannel.send( - `${msg.author} has opened a portal from ${ - msg.channel - } summoning ${msg.mentions.members.map((i) => i).join(" ")}`, - ); - } catch (e) { - console.log("Something went wrong when summoning a portal: ", e); - } - }, - }, - { - words: [`!mdn`], - help: `allows you to search something on MDN, usage: !mdn Array.prototype.map`, - category: "Web", - handleMessage: async (msg) => { - const [, ...args] = msg.content.split(" "); - const query = args.join(" "); - const [fetchMsg, res] = await Promise.all([ - msg.channel.send(`Fetching "${query}"...`), - fetch( - `https://developer.mozilla.org/api/v1/search/en-US?highlight=false&q=${query}`, - ), - ]); - - const { documents } = await res.json(); - const [topResult] = documents; - - if (!topResult) { - fetchMsg.edit(`Could not find anything on MDN for '${query}'`); - return; - } - - const { title, excerpt: description, mdn_url: mdnUrl } = topResult; - - await msg.channel.send({ - embeds: [ - { - author: { - name: "MDN", - url: "https://developer.mozilla.org", - icon_url: - "https://developer.mozilla.org/static/img/opengraph-logo.72382e605ce3.png", - }, - title, - description, - color: 0x83d0f2, - url: `https://developer.mozilla.org${mdnUrl}`, - }, - ], - }); - - fetchMsg.delete(); - }, - }, - { - words: [`!appideas`], + words: [`appideas`], help: `provides a link to the best curated app ideas for beginners to advanced devs`, category: "Web", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "Florinpop17s Curated App Ideas!", @@ -396,11 +407,11 @@ Here's an article explaining the difference between the two: https://goshakkk.na }, }, { - words: [`!cors`], + words: [`cors`], help: `provides a link to what CORS is and how to fix it`, category: "Web", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "Understanding CORS", @@ -420,11 +431,11 @@ Here's an article explaining the difference between the two: https://goshakkk.na }, }, { - words: [`!imm`, `!immutability`], + words: [`imm`, `immutability`], help: `provides resources for helping with immutability`, category: "React/Redux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "Immutable updates", @@ -442,11 +453,11 @@ Here's an article explaining the difference between the two: https://goshakkk.na }, }, { - words: [`!redux`], + words: [`redux`], help: `Info and when and why to use Redux`, category: "React/Redux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "When should you use Redux?", @@ -467,11 +478,11 @@ Here's an article explaining the difference between the two: https://goshakkk.na }, }, { - words: [`!reduxvscontext`, "!context"], + words: [`reduxvscontext`, "context"], help: `Differences between Redux and Context`, category: "React/Redux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "What are the differences between Redux and Context?", @@ -496,11 +507,11 @@ Here's an article explaining the difference between the two: https://goshakkk.na }, }, { - words: [`!render`], + words: [`render`], help: `Explanation of how React rendering behavior works`, category: "React/Redux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "How does React rendering behavior work?", @@ -523,11 +534,11 @@ Here's an article explaining the difference between the two: https://goshakkk.na }, }, { - words: [`!formatting`, `!prettier`], + words: [`formatting`, `prettier`], help: `describes Prettier and explains how to use it to format code`, category: "Reactiflux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "Formatting code with Prettier", @@ -546,11 +557,11 @@ To integrate it into your editor: https://prettier.io/docs/en/editors.html`, }, }, { - words: [`!gender`], + words: [`gender`], help: `reminds users to use gender-neutral language`, category: "Communication", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "Please use gender neutral language by default", @@ -567,11 +578,11 @@ To integrate it into your editor: https://prettier.io/docs/en/editors.html`, }, }, { - words: [`!reactts`], + words: [`reactts`], help: `Resources and tips for using React + TypeScript together`, category: "React/Redux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "Resources for React + TypeScript", @@ -593,11 +604,11 @@ To integrate it into your editor: https://prettier.io/docs/en/editors.html`, }, }, { - words: [`!hooks`, `!learn`], + words: [`hooks`, `learn`], help: `Resources for Learning React`, category: "React/Redux", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "Learning React", @@ -615,11 +626,11 @@ To integrate it into your editor: https://prettier.io/docs/en/editors.html`, }, }, { - words: [`!nw`, `!notworking`], + words: [`nw`, `notworking`], help: `gives some tips on how to improve your chances at getting an answer`, category: "Communication", handleMessage: (msg) => { - msg.channel.send({ + msg.channel?.send({ embeds: [ { title: "State your problem", @@ -637,16 +648,16 @@ Instead: }, }, { - words: ["!lock"], - help: "", + words: ["lock"], + help: "locks a channel", category: "Communication", handleMessage: async (msg) => { - if (!msg.guild || !isStaff(msg.member)) { + if (!(await isStaffMsg(msg))) { return; } // permission overwrites can only be applied on Guild Text Channels - if (msg.channel.type === "GUILD_TEXT") { + if (msg.channel?.type === "GUILD_TEXT") { const { channel: guildTextChannel } = msg; await guildTextChannel.permissionOverwrites.create( guildTextChannel.guild.roles.everyone, @@ -659,16 +670,16 @@ Instead: }, }, { - words: ["!unlock"], - help: "", + words: ["unlock"], + help: "unlocks a channel", category: "Communication", handleMessage: async (msg) => { - if (!msg.guild || !isStaff(msg.member)) { + if (!(await isStaffMsg(msg))) { return; } // permission overwrites can only be applied on Guild Text Channels - if (msg.channel.type === "GUILD_TEXT") { + if (msg.channel?.type === "GUILD_TEXT") { const { channel: guildTextChannel } = msg; guildTextChannel.permissionOverwrites.create( guildTextChannel.guild.roles.everyone, @@ -707,7 +718,7 @@ const createCommandsMessage = () => { const boldTitle = `**${category}**`; const commandDescriptions = commands .map((command) => { - const formattedWords = command.words.map((word) => `**\`${word}\`**`); + const formattedWords = command.words.map((word) => `**\`!${word}\`**`); return `${formattedWords.join(", ")}: ${command.help}`; }) .join("\n"); @@ -730,7 +741,7 @@ const commands: ChannelHandlers = { commandsList.forEach((command) => { const keyword = command.words.find((word) => { - return msg.content.toLowerCase().includes(word); + return msg.content.toLowerCase().includes(`!${word}`); }); if (keyword) { diff --git a/src/index.ts b/src/index.ts index 658d1411..50b75ca4 100644 --- a/src/index.ts +++ b/src/index.ts @@ -5,13 +5,18 @@ import discord, { Intents, PartialMessageReaction, PartialUser, + Interaction, } from "discord.js"; +import { SlashCommandBuilder } from "@discordjs/builders"; +import { REST } from "@discordjs/rest"; +import { Routes } from "discord-api-types/v9"; import { logger, stdoutLog, channelLog } from "./features/log"; // import codeblock from './features/codeblock'; import jobsMod from "./features/jobs-moderation"; import autoban from "./features/autoban"; -import commands from "./features/commands"; +import commands, { commandsList } from "./features/commands"; +import cooldown from "./features/cooldown"; import setupStats from "./features/stats"; import emojiMod from "./features/emojiMod"; import autodelete from "./features/autodelete-spam"; @@ -36,6 +41,9 @@ export const bot = new discord.Client({ partials: ["MESSAGE", "CHANNEL", "REACTION"], }); +const api = new REST({ version: "9" }); +if (process.env.DISCORD_HASH) api.setToken(process.env.DISCORD_HASH); + bot .login(process.env.DISCORD_HASH) .then(async () => { @@ -56,6 +64,18 @@ bot if (bot.application) { const { id } = bot.application; + + await api.put(Routes.applicationCommands(id), { + body: commandsList.flatMap((command) => + command.words.map((word) => { + return new SlashCommandBuilder() + .setName(word) + .setDescription(command.help) + .toJSON(); + }), + ), + }); + console.log("Bot started. If necessary, add it to your test server:"); console.log( `https://discord.com/oauth2/authorize?client_id=${id}&scope=bot`, @@ -147,6 +167,28 @@ if (process.env.BOT_LOG) { logger.add(channelLog(bot, process.env.BOT_LOG)); // #bot-log } +const handleInteraction = async (interaction: Interaction) => { + if (!interaction.isCommand()) return; + + commandsList.forEach((command) => { + const keyword = command.words.find((word) => { + return interaction.commandName == word; + }); + + if (keyword) { + if (cooldown.hasCooldown(interaction.user.id, `commands.${keyword}`)) { + return; + } + cooldown.addCooldown( + interaction.user.id, + `commands.${keyword}`, + command.cooldown, + ); + command.handleMessage(interaction); + } + }); +}; + // Amplitude metrics setupStats(bot); @@ -181,6 +223,8 @@ bot.on("messageCreate", async (msg) => { handleMessage(msg); }); +bot.on("interactionCreate", handleInteraction); + bot.on("error", (err) => { try { logger.log("ERR", err.message); diff --git a/yarn.lock b/yarn.lock index 050189f8..3da9d222 100644 --- a/yarn.lock +++ b/yarn.lock @@ -13,11 +13,29 @@ tslib "^2.3.1" zod "^3.11.6" +"@discordjs/collection@^0.3.2": + version "0.3.2" + resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.3.2.tgz#3c271dd8a93dad89b186d330e24dbceaab58424a" + integrity sha512-dMjLl60b2DMqObbH1MQZKePgWhsNe49XkKBZ0W5Acl5uVV43SN414i2QfZwRI7dXAqIn8pEWD2+XXQFn9KWxqg== + "@discordjs/collection@^0.4.0": version "0.4.0" resolved "https://registry.yarnpkg.com/@discordjs/collection/-/collection-0.4.0.tgz#b6488286a1cc7b41b644d7e6086f25a1c1e6f837" integrity sha512-zmjq+l/rV35kE6zRrwe8BHqV78JvIh2ybJeZavBi5NySjWXqN3hmmAKg7kYMMXSeiWtSsMoZ/+MQi0DiQWy2lw== +"@discordjs/rest@^0.2.0-canary.0": + version "0.2.0-canary.0" + resolved "https://registry.yarnpkg.com/@discordjs/rest/-/rest-0.2.0-canary.0.tgz#5ac955614453d02808c4df35a5cfb28d146566b2" + integrity sha512-jOxz1aqTEzn9N0qaJcZbHz6FbA0oq+vjpXUKkQzgfMihO6gC+kLlpRnFqG25T/aPYbjaR1UM/lGhrGBB1dutqg== + dependencies: + "@discordjs/collection" "^0.3.2" + "@sapphire/async-queue" "^1.1.9" + "@sapphire/snowflake" "^3.0.0" + discord-api-types "^0.25.2" + form-data "^4.0.0" + node-fetch "^2.6.5" + tslib "^2.3.1" + "@eslint/eslintrc@^1.0.5": version "1.0.5" resolved "https://registry.yarnpkg.com/@eslint/eslintrc/-/eslintrc-1.0.5.tgz#33f1b838dbf1f923bfa517e008362b78ddbbf318" @@ -73,6 +91,11 @@ resolved "https://registry.yarnpkg.com/@sapphire/async-queue/-/async-queue-1.1.9.tgz#ce69611c8753c4affd905a7ef43061c7eb95c01b" integrity sha512-CbXaGwwlEMq+l1TRu01FJCvySJ1CEFKFclHT48nIfNeZXaAAmmwwy7scUKmYHPUa3GhoMp6Qr1B3eAJux6XgOQ== +"@sapphire/snowflake@^3.0.0": + version "3.0.0" + resolved "https://registry.yarnpkg.com/@sapphire/snowflake/-/snowflake-3.0.0.tgz#ff64900d69e3a687fdbb9023be91b1092cc7cc1b" + integrity sha512-YVYXvpWe8fVs2P9mvvsMXByXCcSPcsgUhuKwA+SSlJk1VO7EW1vWjlgGozGj0tPOhsuaeAj1EjPbkCmNKiSRLA== + "@sindresorhus/is@^4.2.0": version "4.2.0" resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-4.2.0.tgz#667bfc6186ae7c9e0b45a08960c551437176e1ca" @@ -408,11 +431,21 @@ dir-glob@^3.0.1: dependencies: path-type "^4.0.0" +discord-api-types@^0.25.2: + version "0.25.2" + resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.25.2.tgz#e50ed152e6d48fe7963f5de1002ca6f2df57c61b" + integrity sha512-O243LXxb5gLLxubu5zgoppYQuolapGVWPw3ll0acN0+O8TnPUE2kFp9Bt3sTRYodw8xFIknOVxjSeyWYBpVcEQ== + discord-api-types@^0.26.0: version "0.26.0" resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.26.0.tgz#0134c6ee919035f2075ac1af9cdc0898b8dae71d" integrity sha512-bnUltSHpQLzTVZTMjm+iNgVhAbtm5oAKHrhtiPaZoxprbm1UtuCZCsG0yXM61NamWfeSz7xnLvgFc50YzVJ5cQ== +discord-api-types@^0.26.1: + version "0.26.1" + resolved "https://registry.yarnpkg.com/discord-api-types/-/discord-api-types-0.26.1.tgz#726f766ddc37d60da95740991d22cb6ef2ed787b" + integrity sha512-T5PdMQ+Y1MEECYMV5wmyi9VEYPagEDEi4S0amgsszpWY0VB9JJ/hEvM6BgLhbdnKky4gfmZEXtEEtojN8ZKJQQ== + discord.js@^13.5.0: version "13.5.0" resolved "https://registry.yarnpkg.com/discord.js/-/discord.js-13.5.0.tgz#f9ca9e629f2de0fb138e8c916fa93e40d70631f5" @@ -971,7 +1004,7 @@ needle@^2.1.1, needle@^2.2.2: iconv-lite "^0.4.4" sax "^1.2.4" -node-fetch@^2.2.0, node-fetch@^2.6.1: +node-fetch@^2.2.0, node-fetch@^2.6.1, node-fetch@^2.6.5: version "2.6.6" resolved "https://registry.yarnpkg.com/node-fetch/-/node-fetch-2.6.6.tgz#1751a7c01834e8e1697758732e9efb6eeadfaf89" integrity sha512-Z8/6vRlTUChSdIgMa51jxQ4lrw/Jy5SOW10ObaA47/RElsAN2c5Pn8bTgFGWn/ibwzXTE8qwr1Yzx28vsecXEA==