Compare commits

...

22 Commits

Author SHA1 Message Date
1336ee7d0d igore DMs and split content by words 2024-04-05 14:17:29 +02:00
e2ef65203e prepare messageCreate event 2024-04-05 13:51:58 +02:00
272525ad9c force lowercase response names for case insensitiviy 2024-04-05 13:51:14 +02:00
089ccade66 clean up: consistency 2024-04-05 11:56:24 +02:00
c666f5bf8d implement and categorized info methods and completion 2024-04-05 11:47:50 +02:00
904c33c2f8 bugfix: abort if not found 2024-04-05 11:47:37 +02:00
765e80071a use shared method for keyword autocomplete 2024-04-05 11:36:33 +02:00
1d5d22dd5f optional parameter to pass focused value 2024-04-05 11:33:13 +02:00
03fd826952 acknowledge with reply 2024-04-05 11:32:31 +02:00
54dc8bd245 implement removal methods 2024-04-05 11:19:12 +02:00
d10dd2da00 autocomplete remove categories 2024-04-05 11:12:43 +02:00
ab53486ab3 bugfix: find responses only by keyword 2024-04-05 11:12:16 +02:00
0688dad57b abort submit if response name is taken 2024-04-05 10:57:32 +02:00
b812d0e74d categorize remove operations 2024-04-05 10:54:49 +02:00
2586ffb155 respond to modalSubmit event 2024-04-05 10:52:53 +02:00
fb17c4d10b bugfix: adjusted parameters accordingly 2024-04-05 10:52:20 +02:00
601c9db54f bugfix: display name instead of value 2024-04-05 10:51:36 +02:00
370361f9d3 'none found' fallback response 2024-04-05 10:51:17 +02:00
7f39be34ad bugfix: find responses by keyword 2024-04-05 10:51:01 +02:00
9b4d5a1d02 bugfix: adjusted parameters accordingly 2024-04-05 10:31:06 +02:00
7672748ac8 bugfix: adjusted relative paths from major refactor 2024-04-05 10:30:41 +02:00
aed1d8472e properly extracting parameters 2024-04-05 10:16:59 +02:00
10 changed files with 275 additions and 91 deletions

View File

@ -4,7 +4,7 @@ import {
SlashCommandBuilder, SlashCommandBuilder,
ChatInputCommandInteraction ChatInputCommandInteraction
} from 'discord.js'; } from 'discord.js';
import { Guilds, VoiceChannels } from '../../../database.js'; import { Guilds, VoiceChannels } from '../../database.js';
export const data = new SlashCommandBuilder() export const data = new SlashCommandBuilder()
.setName('custom_vc') .setName('custom_vc')

View File

@ -1,5 +1,5 @@
import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js'; import { SlashCommandBuilder, PermissionFlagsBits, ChatInputCommandInteraction } from 'discord.js';
import { Roles, Guilds } from '../../../database.js'; import { Roles, Guilds } from '../../database.js';
/** /**
* @param {Guilds} guild * @param {Guilds} guild

View File

@ -115,34 +115,67 @@ async function addResponse(interaction) {
await interaction.showModal(modal); await interaction.showModal(modal);
} }
/** @param {ChatInputCommandInteraction} interaction */
async function removeKeyword(interaction) {
const { options } = interaction;
// Get command options
const keyword = options.getString('name');
// Try deleting keyword from database
await Keywords.destroy({
where: {
guild: interaction.guildId,
name: keyword
}
});
// Reply with success
await interaction.reply({
content: `Keyword '${keyword}' was successfully deleted!`,
ephemeral: true
});
}
/** @param {ChatInputCommandInteraction} interaction */ /** @param {ChatInputCommandInteraction} interaction */
async function removeResponse(interaction) { async function removeResponse(interaction) {
const { options } = interaction; const { options } = interaction;
// Get command options // Get command options
/** @type {'keyword'|'response'} */ const keyword = options.getString('keyword');
const type = options.getString('type');
const name = options.getString('name'); const name = options.getString('name');
switch (type) { // Find keyword in database
case 'keyword': /** @type {import('../../models/keywords.js').Keyword|null} */
// Try removing keyword const found = await Keywords.findOne({
await Keywords.destroy({
where: { where: {
guild: interaction.guildId, guild: interaction.guildId,
name name: keyword
} }
}); });
break;
case 'response': // Abort if keyword not found
// Try removing response if (found === null) {
await interaction.reply({
content: 'Unknown keyword was specified!',
ephemeral: true
});
return;
}
// Try deleting response from database
await Responses.destroy({ await Responses.destroy({
where: { where: {
guild: interaction.guildId, keyword: found.id,
name name
} }
}); });
}
// Reply with success
await interaction.reply({
content: `Response with name '${name}' was successfully deleted!`,
ephemeral: true
});
} }
/** @param {ChatInputCommandInteraction} interaction */ /** @param {ChatInputCommandInteraction} interaction */
@ -175,11 +208,62 @@ async function listResponse(interaction) {
} }
/** @param {ChatInputCommandInteraction} interaction */ /** @param {ChatInputCommandInteraction} interaction */
async function infoResponse(interaction) { async function responseInfos(interaction) {
const { options } = interaction; const { options } = interaction;
// Get command options // Get command options
const keyword = options.getString('keyword'); const keyword = options.getString('keyword');
const name = options.getString('name');
// Find keyword in database
/** @type {import('../../models/keywords.js').Keyword|null} */
const found = await Keywords.findOne({
where: {
guild: interaction.guildId,
name: keyword
}
});
// Abort if keyword not found
if (found === null) {
await interaction.reply({
content: 'Unknown keyword was specified!',
ephemeral: true
});
return;
}
// Find response in database
/** @type {import('../../models/responses.js').Response|null} */
const response = await Responses.findOne({
where: {
keyword: found.id,
name
}
});
// Abort if response not found
if (response === null) {
await interaction.reply({
content: 'Unknown response was specified!',
ephemeral: true
});
return;
}
// Reply with success
await interaction.reply({
content: `Response with name '${name}' has data of \`${response.response}\`!`,
ephemeral: true
});
}
/** @param {ChatInputCommandInteraction} interaction */
async function keywordInfos(interaction) {
const { options } = interaction;
// Get command options
const keyword = options.getString('name');
// Find keyword in database // Find keyword in database
/** @type {import('../../models/keywords.js').Keyword|null} */ /** @type {import('../../models/keywords.js').Keyword|null} */
@ -203,13 +287,21 @@ async function infoResponse(interaction) {
/** @type {import('../../models/responses.js').Response[]} */ /** @type {import('../../models/responses.js').Response[]} */
const responses = await Responses.findAll({ const responses = await Responses.findAll({
where: { where: {
guild: interaction.guildId, keyword: found.id
name: found.id
} }
}); });
// Abort if no responses registered
if (responses.length === 0) {
await interaction.reply({
content: 'No responses have been registered yet!',
ephemeral: true
});
return;
}
// Join list of responses // Join list of responses
const joined = responses.map((response) => response.response).join('\n- '); const joined = responses.map((response) => response.name).join('\n- ');
// Reply with list of responses // Reply with list of responses
await interaction.reply({ await interaction.reply({
@ -218,11 +310,13 @@ async function infoResponse(interaction) {
}); });
} }
/** /** @param {AutocompleteInteraction} interaction */
* @param {string} guildId async function keywordAutocomplete(interaction, focused) {
* @param {string} focused const { options, guildId } = interaction;
*/
async function keywordAutocomplete(guildId, focused) { // Get command options
if (!focused) focused = options.getFocused();
// Get list of keywords from database // Get list of keywords from database
/** @type {import('../../models/keywords.js').Keyword[]} */ /** @type {import('../../models/keywords.js').Keyword[]} */
const keywords = await Keywords.findAll({ const keywords = await Keywords.findAll({
@ -239,11 +333,15 @@ async function keywordAutocomplete(guildId, focused) {
} }
/** /**
* @param {string} guildId * @param {AutocompleteInteraction} interaction
* @param {string} focused * @param {string} focused
* @param {string} keyword
*/ */
async function responseAutocomplete(guildId, focused, keyword) { async function completeResponses(interaction, focused) {
const { options, guildId } = interaction;
// Get command options
const keyword = options.getString('keyword');
// Get keyword // Get keyword
/** @type {import('../../models/keywords.js').Keyword} */ /** @type {import('../../models/keywords.js').Keyword} */
const found = await Keywords.findOne({ const found = await Keywords.findOne({
@ -260,7 +358,6 @@ async function responseAutocomplete(guildId, focused, keyword) {
? [] ? []
: await Responses.findAll({ : await Responses.findAll({
where: { where: {
guild: guildId,
keyword: found.id keyword: found.id
} }
}); });
@ -269,27 +366,22 @@ async function responseAutocomplete(guildId, focused, keyword) {
const filtered = responses.filter((choice) => choice.name.startsWith(focused)); const filtered = responses.filter((choice) => choice.name.startsWith(focused));
// Respond with possible suggestions // Respond with possible suggestions
await interaction.respond( await interaction.respond(filtered.map((choice) => ({ name: choice.name, value: choice.name })));
filtered.map((choice) => ({ name: choice.name, value: choice.response }))
);
} }
/** @param {AutocompleteInteraction} interaction */ /** @param {AutocompleteInteraction} interaction */
async function removeAutocomplete(interaction) { async function responseAutocomplete(interaction) {
const { options } = interaction; const { options } = interaction;
const type = options.getString('type'); // Get command options
const focused = options.getFocused(true);
switch (type) { const { name, value } = focused;
switch (name) {
case 'keyword': case 'keyword':
await keywordAutocomplete(interaction.guildId, options.getFocused()); keywordAutocomplete(interaction, value);
break; break;
case 'response': case 'name':
await responseAutocomplete( completeResponses(interaction, value);
interaction.guildId,
options.getFocused(),
options.getString('keyword')
);
break; break;
} }
} }
@ -322,20 +414,33 @@ export const data = new SlashCommandBuilder()
.setRequired(true) .setRequired(true)
) )
) )
.addSubcommandGroup((group) =>
group
.setName('remove')
.setDescription('Unregisters a response or a keyword.')
.addSubcommand((subcommand) => .addSubcommand((subcommand) =>
subcommand subcommand
.setName('remove') .setName('keyword')
.setDescription('Unregisters a response to a keyword.') .setDescription('Deletes a keyword completely.')
.addStringOption((option) => .addStringOption((option) =>
option option
.setName('type') .setName('name')
.setDescription('The type of data to be removed.') .setDescription('The keyword to be deleted.')
.setAutocomplete(true)
.setRequired(true) .setRequired(true)
.addChoices(
{ name: 'Keyword', value: 'keyword' },
{ name: 'Response', value: 'response' }
) )
) )
.addSubcommand((subcommand) =>
subcommand
.setName('response')
.setDescription('Unregisters a response of a keyword.')
.addStringOption((option) =>
option
.setName('keyword')
.setDescription('The keyword that would trigger the response.')
.setAutocomplete(true)
.setRequired(true)
)
.addStringOption((option) => .addStringOption((option) =>
option option
.setName('name') .setName('name')
@ -344,29 +449,54 @@ export const data = new SlashCommandBuilder()
.setRequired(true) .setRequired(true)
) )
) )
)
.addSubcommand((subcommand) => .addSubcommand((subcommand) =>
subcommand.setName('list').setDescription('Lists all registered keywords.') subcommand.setName('list').setDescription('Lists all registered keywords.')
) )
.addSubcommandGroup((group) =>
group
.setName('info')
.setDescription('Lists information about a response or a keyword.')
.addSubcommand((subcommand) => .addSubcommand((subcommand) =>
subcommand subcommand
.setName('info') .setName('keyword')
.setDescription('Shows responses of a registered keyword.') .setDescription('Lists registered responses of a keyword.')
.addStringOption((option) =>
option
.setName('name')
.setDescription('The keyword to be shown the details of.')
.setAutocomplete(true)
.setRequired(true)
)
)
.addSubcommand((subcommand) =>
subcommand
.setName('response')
.setDescription('Lists the data being sent by a response.')
.addStringOption((option) => .addStringOption((option) =>
option option
.setName('keyword') .setName('keyword')
.setDescription('The keyword to show the details of.') .setDescription('The keyword that would trigger the response.')
.setAutocomplete(true) .setAutocomplete(true)
.setRequired(true) .setRequired(true)
) )
.addStringOption((option) =>
option
.setName('name')
.setDescription('The name of the data to be listed.')
.setAutocomplete(true)
.setRequired(true)
)
)
); );
/** @param {ModalSubmitInteraction} interaction */ /** @param {ModalSubmitInteraction} interaction */
export async function modalSubmit(interaction) { export async function modalSubmit(interaction) {
const { fields } = interaction; const { fields } = interaction;
// Get text inputs from modal // Get text inputs from modal
const name = fields.getTextInputValue('name');
const keyword = fields.getTextInputValue('keyword'); const keyword = fields.getTextInputValue('keyword');
const response = fields.getTextInputValue('response'); const response = fields.getTextInputValue('response');
const name = fields.getTextInputValue('name').toLowerCase();
// Get id of keyword // Get id of keyword
/** @type {import('../../models/keywords.js').Keyword} */ /** @type {import('../../models/keywords.js').Keyword} */
@ -376,20 +506,50 @@ export async function modalSubmit(interaction) {
} }
}); });
// Abort if response exists
if (
(await Responses.findOne({
where: {
keyword: found.id,
name
}
})) !== null
) {
await interaction.reply({
content: `Response with name '${name}' already exists!`,
ephemeral: true
});
return;
}
// Create new response data with keyword attached // Create new response data with keyword attached
await Responses.create({ keyword: found.id, name, response }); await Responses.create({ keyword: found.id, name, response });
// Reply with success
await interaction.reply({
content: `Successfully registered '${name}' as response to '${keyword}'!`,
ephemeral: true
});
} }
/** @param {AutocompleteInteraction} interaction */ /** @param {AutocompleteInteraction} interaction */
export async function autocomplete(interaction) { export async function autocomplete(interaction) {
const { options } = interaction; const { options } = interaction;
switch (options.getSubcommand()) { const command = options.getSubcommand();
case 'remove': const group = options.getSubcommandGroup();
removeAutocomplete(interaction); const joined = group === null ? command : `${group} ${command}`;
switch (joined) {
case 'info keyword':
case 'remove keyword':
await keywordAutocomplete(interaction);
break;
case 'info response':
case 'remove response':
await responseAutocomplete(interaction);
break; break;
case 'add': case 'add':
case 'info': keywordAutocomplete(interaction);
keywordAutocomplete(interaction.guildId, interaction.options.getFocused());
break; break;
} }
} }
@ -397,21 +557,31 @@ export async function autocomplete(interaction) {
export async function execute(interaction) { export async function execute(interaction) {
const { options } = interaction; const { options } = interaction;
switch (options.getSubcommand()) { const command = options.getSubcommand();
const group = options.getSubcommandGroup();
const joined = group === null ? command : `${group} ${command}`;
switch (joined) {
case 'create': case 'create':
createResponse(interaction); createResponse(interaction);
break; break;
case 'add': case 'add':
addResponse(interaction); addResponse(interaction);
break; break;
case 'remove': case 'remove keyword':
removeKeyword(interaction);
break;
case 'remove response':
removeResponse(interaction); removeResponse(interaction);
break; break;
case 'list': case 'list':
listResponse(interaction); listResponse(interaction);
break; break;
case 'info': case 'info keyword':
infoResponse(interaction); keywordInfos(interaction);
break;
case 'info response':
responseInfos(interaction);
break; break;
} }
} }

View File

@ -9,7 +9,7 @@ import {
ContextMenuCommandBuilder, ContextMenuCommandBuilder,
ContextMenuCommandInteraction ContextMenuCommandInteraction
} from 'discord.js'; } from 'discord.js';
import { addSelfRoles } from '../../../../shared.js'; import { addSelfRoles } from '../../../shared.js';
export const data = new ContextMenuCommandBuilder() export const data = new ContextMenuCommandBuilder()
.setDMPermission(false) .setDMPermission(false)

View File

@ -4,7 +4,7 @@ import {
PermissionFlagsBits, PermissionFlagsBits,
ContextMenuCommandInteraction ContextMenuCommandInteraction
} from 'discord.js'; } from 'discord.js';
import { Messages } from '../../../../database.js'; import { Messages } from '../../../database.js';
export const data = new ContextMenuCommandBuilder() export const data = new ContextMenuCommandBuilder()
.setDMPermission(false) .setDMPermission(false)

View File

@ -4,7 +4,7 @@ import {
PermissionFlagsBits, PermissionFlagsBits,
ContextMenuCommandInteraction ContextMenuCommandInteraction
} from 'discord.js'; } from 'discord.js';
import { removeSelfRoles } from '../../../../shared.js'; import { removeSelfRoles } from '../../../shared.js';
export const data = new ContextMenuCommandBuilder() export const data = new ContextMenuCommandBuilder()
.setDMPermission(false) .setDMPermission(false)

View File

@ -1,6 +1,6 @@
import { PermissionFlagsBits, SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js'; import { PermissionFlagsBits, SlashCommandBuilder, ChatInputCommandInteraction } from 'discord.js';
import { addSelfRoles, removeSelfRoles } from '../../../shared.js'; import { addSelfRoles, removeSelfRoles } from '../../shared.js';
import { Guilds, Messages } from '../../../database.js'; import { Guilds, Messages } from '../../database.js';
/** /**
* Sends a `Message` in the current channel and registers for self roles. * Sends a `Message` in the current channel and registers for self roles.

View File

@ -9,7 +9,7 @@ import Module from 'module';
const executeCommand = async (interaction, command) => { const executeCommand = async (interaction, command) => {
// Try executing command // Try executing command
try { try {
console.info(`[INFO] Command ${interaction.commandName} was executed.`); console.info(`[INFO] Command '${interaction.commandName}' was executed.`);
await command.execute(interaction); await command.execute(interaction);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
@ -38,7 +38,7 @@ const executeCommand = async (interaction, command) => {
const genericExecute = async (interaction, command, name, description, cmdName) => { const genericExecute = async (interaction, command, name, description, cmdName) => {
try { try {
console.info( console.info(
`[INFO] Command ${cmdName ?? interaction.commandName ?? 'anonymous'} ${ `[INFO] Command '${cmdName ?? interaction.commandName ?? 'anonymous'}' ${
description ?? `used "${name}"` description ?? `used "${name}"`
}.` }.`
); );

View File

@ -0,0 +1,13 @@
import { Keywords, Responses, sequelize } from '../../database.js';
import { Events, Message } from 'discord.js';
import { Op } from 'sequelize';
export const name = Events.MessageCreate;
/** @param {Message} message */
export async function execute(message) {
// Ignore direct messages
if (!message.inGuild()) return;
// Split message content into words
const words = message.content.split(/\s+/);
}

View File

@ -19,6 +19,7 @@ const runClient = (commands, events) => {
GatewayIntentBits.Guilds, GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildMessageReactions GatewayIntentBits.GuildMessageReactions
], ],