From 80d4693c3db171c00b55b841fe01a9496a10066b Mon Sep 17 00:00:00 2001 From: Baipyrus Date: Sun, 11 Feb 2024 02:04:12 +0100 Subject: [PATCH] document code with jsdoc comments --- commands/admin/custom_vc/slash.js | 9 +++- commands/admin/self_roles/context/add.js | 10 ++++- commands/admin/self_roles/context/register.js | 8 +++- commands/admin/self_roles/context/remove.js | 8 +++- commands/admin/self_roles/slash.js | 36 ++++++++++++--- database.js | 1 + deploy.js | 10 +++-- events/channels/channelDelete.js | 3 +- events/channels/voiceStateUpdate.js | 27 ++++++++++- events/interactionCreate.js | 16 +++++++ events/messages/messageDelete.js | 3 +- events/messages/reactionAdd.js | 8 +++- events/messages/reactionRemove.js | 8 +++- events/ready.js | 3 +- index.js | 16 ++++++- models/messages.js | 14 +++++- models/roleEmojiPairs.js | 15 ++++++- models/voiceChannels.js | 14 +++++- shared.js | 45 +++++++++++++++++-- 19 files changed, 222 insertions(+), 32 deletions(-) diff --git a/commands/admin/custom_vc/slash.js b/commands/admin/custom_vc/slash.js index 36b1b2e..5692a92 100644 --- a/commands/admin/custom_vc/slash.js +++ b/commands/admin/custom_vc/slash.js @@ -1,4 +1,9 @@ -import { ChannelType, PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; +import { + ChannelType, + PermissionFlagsBits, + SlashCommandBuilder, + ChatInputCommandInteraction +} from 'discord.js'; import { VoiceChannel } from '../../../database.js'; export const data = new SlashCommandBuilder() @@ -41,9 +46,11 @@ export const data = new SlashCommandBuilder() .setDescription('The voice channel to be unregistered.') ) ); +/** @param {ChatInputCommandInteraction} interaction */ export async function execute(interaction) { const { guild, options } = interaction; + /** @type {string} */ let step; try { switch (options.getSubcommand()) { diff --git a/commands/admin/self_roles/context/add.js b/commands/admin/self_roles/context/add.js index 3456acf..c5c8966 100644 --- a/commands/admin/self_roles/context/add.js +++ b/commands/admin/self_roles/context/add.js @@ -1,9 +1,13 @@ -import { PermissionFlagsBits, TextInputBuilder, TextInputStyle } from 'discord.js'; import { ModalBuilder, + TextInputStyle, ActionRowBuilder, + TextInputBuilder, + PermissionFlagsBits, + ModalSubmitInteraction, ApplicationCommandType, - ContextMenuCommandBuilder + ContextMenuCommandBuilder, + ContextMenuCommandInteraction } from 'discord.js'; import { addSelfRoles } from '../../../../shared.js'; @@ -12,6 +16,7 @@ export const data = new ContextMenuCommandBuilder() .setName('Add role emoji pair') .setType(ApplicationCommandType.Message) .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles); +/** @param {ModalSubmitInteraction} interaction */ export async function modalSubmit(interaction) { const { fields, guild } = interaction; // Get text inputs from modal @@ -32,6 +37,7 @@ export async function modalSubmit(interaction) { await addSelfRoles(interaction, message, role, emoji); } +/** @param {ContextMenuCommandInteraction} interaction */ export async function execute(interaction) { const modal = new ModalBuilder() .setCustomId('Add role emoji pair-pair') diff --git a/commands/admin/self_roles/context/register.js b/commands/admin/self_roles/context/register.js index 5612b99..70df442 100644 --- a/commands/admin/self_roles/context/register.js +++ b/commands/admin/self_roles/context/register.js @@ -1,11 +1,17 @@ +import { + ApplicationCommandType, + ContextMenuCommandBuilder, + PermissionFlagsBits, + ContextMenuCommandInteraction +} from 'discord.js'; import { Message } from '../../../../database.js'; -import { ApplicationCommandType, ContextMenuCommandBuilder, PermissionFlagsBits } from 'discord.js'; export const data = new ContextMenuCommandBuilder() .setDMPermission(false) .setName('Register self roles') .setType(ApplicationCommandType.Message) .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles); +/** @param {ContextMenuCommandInteraction} interaction */ export async function execute(interaction) { const id = interaction.targetMessage.id; diff --git a/commands/admin/self_roles/context/remove.js b/commands/admin/self_roles/context/remove.js index c091368..ee038c4 100644 --- a/commands/admin/self_roles/context/remove.js +++ b/commands/admin/self_roles/context/remove.js @@ -1,11 +1,17 @@ +import { + ApplicationCommandType, + ContextMenuCommandBuilder, + PermissionFlagsBits, + ContextMenuCommandInteraction +} from 'discord.js'; import { removeSelfRoles } from '../../../../shared.js'; -import { ApplicationCommandType, ContextMenuCommandBuilder, PermissionFlagsBits } from 'discord.js'; export const data = new ContextMenuCommandBuilder() .setDMPermission(false) .setName('Remove self roles') .setType(ApplicationCommandType.Message) .setDefaultMemberPermissions(PermissionFlagsBits.ManageRoles); +/** @param {ContextMenuCommandInteraction} interaction */ export async function execute(interaction) { const id = interaction.targetMessage.id; await removeSelfRoles(interaction, id); diff --git a/commands/admin/self_roles/slash.js b/commands/admin/self_roles/slash.js index e977007..3e12b59 100644 --- a/commands/admin/self_roles/slash.js +++ b/commands/admin/self_roles/slash.js @@ -1,7 +1,12 @@ -import { addSelfRoles } from '../../../shared.js'; -import { PermissionFlagsBits, SlashCommandBuilder } from 'discord.js'; +import { PermissionFlagsBits, SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'; +import { addSelfRoles, removeSelfRoles } from '../../../shared.js'; import { Message } from '../../../database.js'; +/** + * Sends a `Message` in the current channel and registers for self roles. + * @param {ChatInputCommandInteraction} interaction + * @returns {string} + */ const createSelfRoles = async (interaction) => { const { options, channel } = interaction; @@ -16,6 +21,17 @@ const createSelfRoles = async (interaction) => { return id; }; +/** + * @typedef {Object} SelfRoleResponse + * @property {boolean} success Whether or not the operation was successful. + * @property {string|null} msgID A Discord message ID, if successful. + */ + +/** + * Registers a `Message` for self roles. + * @param {ChatInputCommandInteraction} interaction + * @returns {Promise} + */ const registerSelfRoles = async (interaction) => { const { options, channel } = interaction; const id = options.getString('id'); @@ -50,7 +66,12 @@ const registerSelfRoles = async (interaction) => { return response; }; -const removeSelfRoles = async (interaction, msgID) => { +/** + * Main logic of the 'Self Roles remove' slash command to remove the functionality from a `Message`. + * @param {ChatInputCommandInteraction} interaction + * @param {string} msgID A Discord message ID. + */ +const removeReactionRoles = async (interaction, msgID) => { const { channel } = interaction; try { @@ -66,6 +87,7 @@ const removeSelfRoles = async (interaction, msgID) => { return; } + // Call shared method for further logic await removeSelfRoles(interaction, msgID); }; @@ -124,11 +146,13 @@ export const data = new SlashCommandBuilder() .setDescription('The ID to reference the message to be removed.') ) ); +/** @param {ChatInputCommandInteraction} interaction */ export async function execute(interaction) { const { options } = interaction; - let createNew = false, - id; + /** @type {string=} */ + let id; + let createNew = false; switch (options.getSubcommand()) { case 'create': id = await createSelfRoles(interaction); @@ -153,7 +177,7 @@ export async function execute(interaction) { } case 'remove': { const msgID = options.getString('id'); - await removeSelfRoles(interaction, msgID); + await removeReactionRoles(interaction, msgID); break; } } diff --git a/database.js b/database.js index c4e1222..9c13374 100644 --- a/database.js +++ b/database.js @@ -6,6 +6,7 @@ import { config } from 'dotenv'; config(); const { DB_NAME } = process.env; +/** The database instance used as an ORM in this project. */ const sequelize = new Sequelize({ storage: `${DB_NAME}.sqlite`, dialect: 'sqlite', diff --git a/deploy.js b/deploy.js index 458e210..3ad4320 100644 --- a/deploy.js +++ b/deploy.js @@ -3,13 +3,17 @@ import { REST, Routes } from 'discord.js'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { config } from 'dotenv'; +import Module from 'module'; config(); // Construct and prepare an instance of the REST module const rest = new REST().setToken(process.env.TOKEN); -// and deploy your commands! +/** + * Calls HTTP PUT to register commands in discord. + * @param {Array} commands + */ const putCommands = async (commands) => { try { console.info(`[INFO] Started refreshing ${commands.length} application (/) commands.`); @@ -32,7 +36,7 @@ getFiles(cmdPath) // For each command file .then(async (files) => (await Promise.all(files.map(importAndCheck))) - .filter((module) => module !== 0) - .map((module) => module.data.toJSON()) + .filter(/** @param {(Module|0)} module */ (module) => module !== 0) + .map(/** @param {Module} module */ (module) => module.data.toJSON()) ) .then(putCommands); diff --git a/events/channels/channelDelete.js b/events/channels/channelDelete.js index 78e253f..89eb675 100644 --- a/events/channels/channelDelete.js +++ b/events/channels/channelDelete.js @@ -1,7 +1,8 @@ -import { ChannelType, Events } from 'discord.js'; +import { ChannelType, Events, GuildChannel } from 'discord.js'; import { VoiceChannel } from '../../database.js'; export const name = Events.ChannelDelete; +/** @param {GuildChannel} channel */ export async function execute(channel) { if (channel.type !== ChannelType.GuildVoice) return; diff --git a/events/channels/voiceStateUpdate.js b/events/channels/voiceStateUpdate.js index f75a0c1..88cfa50 100644 --- a/events/channels/voiceStateUpdate.js +++ b/events/channels/voiceStateUpdate.js @@ -1,4 +1,12 @@ -import { ChannelType, Events, PermissionFlagsBits } from 'discord.js'; +import { + ChannelType, + Events, + PermissionFlagsBits, + GuildMember, + GuildChannelManager, + GuildChannel, + VoiceState +} from 'discord.js'; import { VoiceChannel } from '../../database.js'; const vcPermissionOverwrites = [ @@ -17,6 +25,13 @@ const vcPermissionOverwrites = [ PermissionFlagsBits.Speak ]; +/** + * Function that either creates a new custom channel or gets an existing one registered in the database. + * @param {GuildMember} member The member that caused this event. + * @param {GuildChannelManager} guildChs All channels in this guild. + * @param {GuildChannel} channel The channel the member joined for this event to trigger. + * @returns {Promise} The channel, whether it's newly created or not. + */ const getChannel = async (member, guildChs, channel) => { // Check database for existing channel const ownCh = await VoiceChannel.findOne({ @@ -52,6 +67,10 @@ const getChannel = async (member, guildChs, channel) => { return privCh; }; +/** + * Function to delete the voice channel, if and only if the user is currently leaving and it was a custom channel. + * @param {VoiceState} state The previous voice state the user was in. + */ const leftVoiceChat = async (state) => { const { channel } = state; @@ -77,8 +96,12 @@ const leftVoiceChat = async (state) => { }; export const name = Events.VoiceStateUpdate; +/** + * @param {VoiceState} oldState + * @param {VoiceState} newState + */ export async function execute(oldState, newState) { - const { channel } = newState + const { channel } = newState; await leftVoiceChat(oldState); if (!channel) return; diff --git a/events/interactionCreate.js b/events/interactionCreate.js index 4771ecb..cf65eea 100644 --- a/events/interactionCreate.js +++ b/events/interactionCreate.js @@ -1,5 +1,11 @@ import { Events } from 'discord.js'; +import Module from 'module'; +/** + * A more precise execution function specifically to call the main property of a module. + * @param {import('discord.js').Interaction} interaction + * @param {Module} command + */ const executeCommand = async (interaction, command) => { // Try executing command try { @@ -21,6 +27,14 @@ const executeCommand = async (interaction, command) => { } }; +/** + * A generic execution function to call command methods. + * @param {import('discord.js').Interaction} interaction + * @param {Module} command + * @param {string} name + * @param {string=} description + * @param {string=} cmdName + */ const genericExecute = async (interaction, command, name, description, cmdName) => { try { console.info( @@ -35,7 +49,9 @@ const genericExecute = async (interaction, command, name, description, cmdName) }; export const name = Events.InteractionCreate; +/** @param {import('discord.js').Interaction} interaction */ export async function execute(interaction) { + /** @type {Module} */ let command = interaction.client.commands.get(interaction.commandName); // Execute slash- and context-menu-commands diff --git a/events/messages/messageDelete.js b/events/messages/messageDelete.js index 2f26198..4891c01 100644 --- a/events/messages/messageDelete.js +++ b/events/messages/messageDelete.js @@ -1,7 +1,8 @@ -import { Events } from 'discord.js'; import { Message } from '../../database.js'; +import { Events } from 'discord.js'; export const name = Events.MessageDelete; +/** @param {import('discord.js').Message} message */ export async function execute(message) { // Delete message entry once message is deleted itself const count = await Message.destroy({ diff --git a/events/messages/reactionAdd.js b/events/messages/reactionAdd.js index b12a43a..726ab6d 100644 --- a/events/messages/reactionAdd.js +++ b/events/messages/reactionAdd.js @@ -1,10 +1,14 @@ -import { config } from 'dotenv'; -import { Events } from 'discord.js'; +import { Events, MessageReaction, User } from 'discord.js'; import { Message, RoleEmojiPair } from '../../database.js'; +import { config } from 'dotenv'; config(); export const name = Events.MessageReactionAdd; +/** + * @param {MessageReaction} reaction + * @param {User} user + */ export async function execute(reaction, user) { if (user.id === process.env.CLIENT) return; diff --git a/events/messages/reactionRemove.js b/events/messages/reactionRemove.js index f91dd08..c17c3da 100644 --- a/events/messages/reactionRemove.js +++ b/events/messages/reactionRemove.js @@ -1,10 +1,14 @@ -import { config } from 'dotenv'; -import { Events } from 'discord.js'; +import { Events, MessageReaction, User } from 'discord.js'; import { Message, RoleEmojiPair } from '../../database.js'; +import { config } from 'dotenv'; config(); export const name = Events.MessageReactionRemove; +/** + * @param {MessageReaction} reaction + * @param {User} user + */ export async function execute(reaction, user) { if (user.id === process.env.CLIENT) return; diff --git a/events/ready.js b/events/ready.js index 561e67c..a9f4402 100644 --- a/events/ready.js +++ b/events/ready.js @@ -1,7 +1,8 @@ -import { Events } from 'discord.js'; +import { Events, Client } from 'discord.js'; export const name = Events.ClientReady; export const once = true; +/** @param {Client} client */ export function execute(client) { console.info(`[INFO] Ready! Logged in as ${client.user.tag}`); } diff --git a/index.js b/index.js index d39384f..7b7aa55 100644 --- a/index.js +++ b/index.js @@ -1,12 +1,17 @@ -import { Client, Collection, GatewayIntentBits } from 'discord.js'; +import { Client, Collection, GatewayIntentBits, Partials } from 'discord.js'; import { getFiles, importAndCheck } from './shared.js'; -import { Partials } from 'discord.js'; import { join, dirname } from 'path'; import { fileURLToPath } from 'url'; import { config } from 'dotenv'; +import Module from 'module'; config(); +/** + * Main entry point, the bot logs on to discord. + * @param {Array} commands + * @param {Array} events + */ const runClient = (commands, events) => { // Create a new client instance const client = new Client({ @@ -18,8 +23,15 @@ const runClient = (commands, events) => { ], partials: [Partials.Message, Partials.Reaction] }); + + /** + * The commands registered for this client. + * @type {Collection} + */ client.commands = new Collection(); commands.forEach((c) => client.commands.set(c.data.name, c)); + + // Register client events events.forEach((e) => e.once ? client.once(e.name, (...args) => e.execute(...args)) diff --git a/models/messages.js b/models/messages.js index dae0097..fc6f583 100644 --- a/models/messages.js +++ b/models/messages.js @@ -1,5 +1,17 @@ -import { DataTypes } from 'sequelize'; +import { DataTypes, Sequelize } from 'sequelize'; +/** + * @typedef {Object} Message + * @property {string} id A Discord message ID. + * @method hasMany Defines an One-To-Many relationship. + * @param {Object} + */ + +/** + * The definition of the `Message` table in the database. + * @param {Sequelize} sequelize + * @returns {Message} + */ export default function (sequelize) { return sequelize.define('Messages', { id: { diff --git a/models/roleEmojiPairs.js b/models/roleEmojiPairs.js index 9b962c6..3b2541b 100644 --- a/models/roleEmojiPairs.js +++ b/models/roleEmojiPairs.js @@ -1,5 +1,18 @@ -import { DataTypes, Deferrable } from 'sequelize'; +import { DataTypes, Deferrable, Sequelize } from 'sequelize'; +/** + * @typedef {Object} RoleEmojiPair + * @property {string} id A universally unique id, generated by sequelize. + * @property {string} message A Discord message ID as a foreign key reference. + * @property {string} role A Discord role ID. + * @property {string} emoji Either a unicode emoji or a string representation in Discord custom emoji format. + */ + +/** + * The definition of the `RoleEmojiPair` table in the database. + * @param {Sequelize} sequelize + * @returns {RoleEmojiPair} + */ export default function (sequelize) { return sequelize.define('RoleEmojiPairs', { id: { diff --git a/models/voiceChannels.js b/models/voiceChannels.js index f1149c7..c2479a2 100644 --- a/models/voiceChannels.js +++ b/models/voiceChannels.js @@ -1,5 +1,17 @@ -import { DataTypes } from 'sequelize'; +import { DataTypes, Sequelize } from 'sequelize'; +/** + * @typedef {Object} VoiceChannel + * @property {string} id A Discord channel ID. + * @property {boolean} create Whether or not this channel is registered to create customs when joined. + * @property {(string|null)} owner The owner of this channel, if not registered for customs. + */ + +/** + * The definition of the `VoiceChannel` table in the database. + * @param {Sequelize} sequelize + * @returns {VoiceChannel} + */ export default function (sequelize) { return sequelize.define('VoiceChannel', { id: { diff --git a/shared.js b/shared.js index bcec0a2..ceea8a5 100644 --- a/shared.js +++ b/shared.js @@ -1,11 +1,18 @@ -import { join } from 'path'; -import { Op } from 'sequelize'; -import { config } from 'dotenv'; -import { readdir } from 'fs/promises'; +import { ChatInputCommandInteraction, ContextMenuCommandInteraction, Role } from 'discord.js'; import { Message, RoleEmojiPair } from './database.js'; +import { readdir } from 'fs/promises'; +import { config } from 'dotenv'; +import { Op } from 'sequelize'; +import { join } from 'path'; +import Module from 'module'; config(); +/** + * Main logic of the different 'Self Roles remove' commands to remove the functionality from a `Message`. + * @param {(ChatInputCommandInteraction|ContextMenuCommandInteraction)} interaction The interaction related to this command. + * @param {string} id A Discord message ID. + */ export const removeSelfRoles = async (interaction, id) => { // Try deleting message from database const count = await Message.destroy({ @@ -27,6 +34,12 @@ export const removeSelfRoles = async (interaction, id) => { console.info(`[INFO] Removed self roles from message with ID '${id}'.`); }; +/** + * Function to handle saving all the corresponding data for `Message` and `RoleEmojiPair` tables. + * @param {string} id A Discord message ID. + * @param {Role} role + * @param {string} emoji Either a unicode emoji or a string representation in Discord custom emoji format. + */ const saveMessageData = async (id, role, emoji) => { // Try finding message const msg = await Message.findOne({ where: { id } }); @@ -56,6 +69,12 @@ const saveMessageData = async (id, role, emoji) => { await RoleEmojiPair.create({ message: id, role: role.id, emoji }); }; +/** + * Function to handle editing messages in case the message that's getting a new `RoleEmojiPair`, is owned by the bot. + * @param {import('discord.js').Message} message + * @param {Role} role + * @param {string} emoji Either a unicode emoji or a string representation in Discord custom emoji format. + */ const editMessage = async (message, role, emoji) => { if (message.author.id !== process.env.CLIENT) return; @@ -72,6 +91,13 @@ const editMessage = async (message, role, emoji) => { await message.edit(next); }; +/** + * Main logic of the different 'Self Roles add' commands to add a `RoleEmojiPair` to the database and the `Message`. + * @param {(ChatInputCommandInteraction|ContextMenuCommandInteraction)} interaction The interaction related to this command. + * @param {string} msgID A Discord message ID. + * @param {Role} role + * @param {string} emoji Either a unicode emoji or a string representation in Discord custom emoji format. + */ export const addSelfRoles = async (interaction, msgID, role, emoji) => { const { channel } = interaction; @@ -109,9 +135,15 @@ export const addSelfRoles = async (interaction, msgID, role, emoji) => { } }; +// Lists of required and optional attributes of command modules const required = ['data', 'execute']; const optional = ['autocomplete', 'modalSubmit']; +/** + * Recursively scans a directory for all files in it. + * @param {string} dir + * @returns {Array} Array of paths to the files within. + */ export const getFiles = async (dir) => { const dirents = await readdir(dir, { withFileTypes: true }); const files = await Promise.all( @@ -123,6 +155,11 @@ export const getFiles = async (dir) => { return Array.prototype.concat(...files); }; +/** + * Imports and checks a command from a path as a module. + * @param {string} filePath + * @returns {Promise} + */ export const importAndCheck = async (filePath) => { if (!filePath.endsWith('.js') || filePath.endsWith('.example.js')) { // Skip this file