Modernize Discord bot to v14 and Node.js 22
Major upgrades and architectural improvements: - Upgrade Discord.js from v12 to v14.21.0 - Upgrade Node.js from 14 to 22 LTS - Switch to pnpm package manager - Complete rewrite with modern Discord API patterns New Features: - Hybrid command system: prefix commands + slash commands - /sfx slash command with autocomplete for sound discovery - Modern @discordjs/voice integration for audio - Improved voice connection management - Enhanced logging for SFX commands - Multi-stage Docker build for optimized images Technical Improvements: - Modular architecture with services and command handlers - Proper intent management for Discord gateway - Better error handling and logging - Hot-reload capability maintained - Environment variable support - Optimized Docker container with Alpine Linux Breaking Changes: - Moved main entry from index.js to src/index.js - Updated configuration structure for v14 compatibility - Replaced deprecated voice APIs with @discordjs/voice - Updated audio dependencies (opus, ffmpeg) 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
10
src/commands/prefix/dance.js
Normal file
10
src/commands/prefix/dance.js
Normal file
@@ -0,0 +1,10 @@
|
||||
module.exports = {
|
||||
name: 'dance',
|
||||
description: 'Make the bot dance!',
|
||||
|
||||
async execute(message, args, guildConfig) {
|
||||
await message.channel.send(
|
||||
'*┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛ ┏(-_-)┓┏(-_-)┛┗(-_- )┓┗(-_-)┛┏(-_-)┛*'
|
||||
);
|
||||
}
|
||||
};
|
||||
79
src/commands/prefix/funfact.js
Normal file
79
src/commands/prefix/funfact.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class FunFactCommand {
|
||||
constructor() {
|
||||
this.funFactsPath = path.join(__dirname, '..', '..', '..', 'conf', 'funfacts');
|
||||
this.funFacts = [];
|
||||
this.loadFunFacts();
|
||||
this.watchFile();
|
||||
}
|
||||
|
||||
loadFunFacts() {
|
||||
try {
|
||||
if (!fs.existsSync(this.funFactsPath)) {
|
||||
console.log('Fun facts file not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(this.funFactsPath, 'utf-8');
|
||||
this.funFacts = data.split('\n').filter(line => line.trim().length > 0);
|
||||
console.log(`Loaded ${this.funFacts.length} fun facts`);
|
||||
} catch (error) {
|
||||
console.error('Error loading fun facts:', error);
|
||||
}
|
||||
}
|
||||
|
||||
watchFile() {
|
||||
if (fs.existsSync(this.funFactsPath)) {
|
||||
fs.watchFile(this.funFactsPath, (curr, prev) => {
|
||||
if (curr.mtime !== prev.mtime) {
|
||||
console.log('Fun facts file changed, reloading...');
|
||||
this.loadFunFacts();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async execute(message, args, guildConfig) {
|
||||
if (guildConfig.enableFunFacts === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.funFacts.length === 0) {
|
||||
return message.channel.send('No fun facts found!');
|
||||
}
|
||||
|
||||
// Check if a specific fact number was requested
|
||||
let factIndex;
|
||||
const requestedNum = parseInt(args[0]);
|
||||
|
||||
if (!isNaN(requestedNum) && requestedNum > 0 && requestedNum <= this.funFacts.length) {
|
||||
factIndex = requestedNum - 1;
|
||||
} else {
|
||||
factIndex = Math.floor(Math.random() * this.funFacts.length);
|
||||
}
|
||||
|
||||
const displayNum = factIndex + 1;
|
||||
const funFact = this.funFacts[factIndex];
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`FunFact #${displayNum}`)
|
||||
.setColor(0x21c629)
|
||||
.setDescription(funFact);
|
||||
|
||||
await message.channel.send({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
|
||||
const funFactCommand = new FunFactCommand();
|
||||
|
||||
module.exports = {
|
||||
name: 'funfact',
|
||||
description: 'Get a random fun fact',
|
||||
|
||||
async execute(message, args, guildConfig) {
|
||||
await funFactCommand.execute(message, args, guildConfig);
|
||||
}
|
||||
};
|
||||
79
src/commands/prefix/hamfact.js
Normal file
79
src/commands/prefix/hamfact.js
Normal file
@@ -0,0 +1,79 @@
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class HamFactCommand {
|
||||
constructor() {
|
||||
this.hamFactsPath = path.join(__dirname, '..', '..', '..', 'conf', 'hamfacts');
|
||||
this.hamFacts = [];
|
||||
this.loadHamFacts();
|
||||
this.watchFile();
|
||||
}
|
||||
|
||||
loadHamFacts() {
|
||||
try {
|
||||
if (!fs.existsSync(this.hamFactsPath)) {
|
||||
console.log('Ham facts file not found');
|
||||
return;
|
||||
}
|
||||
|
||||
const data = fs.readFileSync(this.hamFactsPath, 'utf-8');
|
||||
this.hamFacts = data.split('\n').filter(line => line.trim().length > 0);
|
||||
console.log(`Loaded ${this.hamFacts.length} ham facts`);
|
||||
} catch (error) {
|
||||
console.error('Error loading ham facts:', error);
|
||||
}
|
||||
}
|
||||
|
||||
watchFile() {
|
||||
if (fs.existsSync(this.hamFactsPath)) {
|
||||
fs.watchFile(this.hamFactsPath, (curr, prev) => {
|
||||
if (curr.mtime !== prev.mtime) {
|
||||
console.log('Ham facts file changed, reloading...');
|
||||
this.loadHamFacts();
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
async execute(message, args, guildConfig) {
|
||||
if (guildConfig.enableHamFacts === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.hamFacts.length === 0) {
|
||||
return message.channel.send('No ham facts found!');
|
||||
}
|
||||
|
||||
// Check if a specific fact number was requested
|
||||
let factIndex;
|
||||
const requestedNum = parseInt(args[0]);
|
||||
|
||||
if (!isNaN(requestedNum) && requestedNum > 0 && requestedNum <= this.hamFacts.length) {
|
||||
factIndex = requestedNum - 1;
|
||||
} else {
|
||||
factIndex = Math.floor(Math.random() * this.hamFacts.length);
|
||||
}
|
||||
|
||||
const displayNum = factIndex + 1;
|
||||
const hamFact = this.hamFacts[factIndex];
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`HamFact #${displayNum}`)
|
||||
.setColor(0x21c629)
|
||||
.setDescription(hamFact);
|
||||
|
||||
await message.channel.send({ embeds: [embed] });
|
||||
}
|
||||
}
|
||||
|
||||
const hamFactCommand = new HamFactCommand();
|
||||
|
||||
module.exports = {
|
||||
name: 'hamfact',
|
||||
description: 'Get a random ham fact',
|
||||
|
||||
async execute(message, args, guildConfig) {
|
||||
await hamFactCommand.execute(message, args, guildConfig);
|
||||
}
|
||||
};
|
||||
67
src/commands/prefix/index.js
Normal file
67
src/commands/prefix/index.js
Normal file
@@ -0,0 +1,67 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class PrefixCommandHandler {
|
||||
constructor() {
|
||||
this.commands = new Map();
|
||||
this.loadCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all prefix command modules
|
||||
*/
|
||||
loadCommands() {
|
||||
const commandFiles = fs.readdirSync(__dirname)
|
||||
.filter(file => file.endsWith('.js') && file !== 'index.js');
|
||||
|
||||
for (const file of commandFiles) {
|
||||
const command = require(path.join(__dirname, file));
|
||||
|
||||
// Register command and any aliases
|
||||
if (command.name) {
|
||||
this.commands.set(command.name, command);
|
||||
|
||||
if (command.aliases && Array.isArray(command.aliases)) {
|
||||
for (const alias of command.aliases) {
|
||||
this.commands.set(alias, command);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Loaded ${this.commands.size} prefix commands`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a command exists
|
||||
* @param {string} commandName
|
||||
* @returns {boolean}
|
||||
*/
|
||||
has(commandName) {
|
||||
return this.commands.has(commandName);
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a command
|
||||
* @param {string} commandName
|
||||
* @param {Message} message
|
||||
* @param {Array} args
|
||||
* @param {Object} guildConfig
|
||||
*/
|
||||
async execute(commandName, message, args, guildConfig) {
|
||||
const command = this.commands.get(commandName);
|
||||
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await command.execute(message, args, guildConfig);
|
||||
} catch (error) {
|
||||
console.error(`Error executing prefix command ${commandName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new PrefixCommandHandler();
|
||||
26
src/commands/prefix/join.js
Normal file
26
src/commands/prefix/join.js
Normal file
@@ -0,0 +1,26 @@
|
||||
const voiceService = require('../../services/voiceService');
|
||||
|
||||
module.exports = {
|
||||
name: 'join',
|
||||
description: 'Make the bot join your voice channel',
|
||||
|
||||
async execute(message, args, guildConfig) {
|
||||
// Check if user is in a voice channel
|
||||
if (!message.member.voice.channel) {
|
||||
return message.reply('You need to be in a voice channel first!');
|
||||
}
|
||||
|
||||
// Check if already connected
|
||||
if (voiceService.isConnected(message.guild.id)) {
|
||||
return message.reply("I'm already in a voice channel!");
|
||||
}
|
||||
|
||||
try {
|
||||
await voiceService.join(message.member.voice.channel);
|
||||
await message.react('✅');
|
||||
} catch (error) {
|
||||
console.error('Error joining voice channel:', error);
|
||||
await message.reply("I couldn't connect to your voice channel. Make sure I have the proper permissions!");
|
||||
}
|
||||
}
|
||||
};
|
||||
16
src/commands/prefix/leave.js
Normal file
16
src/commands/prefix/leave.js
Normal file
@@ -0,0 +1,16 @@
|
||||
const voiceService = require('../../services/voiceService');
|
||||
|
||||
module.exports = {
|
||||
name: 'leave',
|
||||
description: 'Make the bot leave the voice channel',
|
||||
|
||||
async execute(message, args, guildConfig) {
|
||||
// Check if connected to a voice channel
|
||||
if (!voiceService.isConnected(message.guild.id)) {
|
||||
return message.reply("If ya don't eat your meat, ya can't have any pudding!");
|
||||
}
|
||||
|
||||
voiceService.leave(message.guild.id);
|
||||
await message.react('👋');
|
||||
}
|
||||
};
|
||||
19
src/commands/prefix/reboot.js
Normal file
19
src/commands/prefix/reboot.js
Normal file
@@ -0,0 +1,19 @@
|
||||
const config = require("../../config/config");
|
||||
|
||||
module.exports = {
|
||||
name: "reboot",
|
||||
description: "Reboot the bot (admin only)",
|
||||
|
||||
async execute(message, args, guildConfig) {
|
||||
// Check if user is the bot admin
|
||||
if (message.author.id !== config.discord.adminUserId) {
|
||||
return;
|
||||
}
|
||||
|
||||
await message.reply("Rebooting...");
|
||||
console.log(`Reboot requested by ${message.author.username}`);
|
||||
|
||||
// Exit the process - requires a process manager like PM2 or Docker restart policy
|
||||
process.exit(0);
|
||||
},
|
||||
};
|
||||
74
src/commands/prefix/role.js
Normal file
74
src/commands/prefix/role.js
Normal file
@@ -0,0 +1,74 @@
|
||||
module.exports = {
|
||||
name: 'role',
|
||||
description: 'Add or remove allowed roles',
|
||||
|
||||
async execute(message, args, guildConfig) {
|
||||
// Check if there are allowed roles configured
|
||||
if (!guildConfig.allowedRolesForRequest || guildConfig.allowedRolesForRequest.length === 0) {
|
||||
return message.reply('No roles are currently allowed to be added/removed by members.');
|
||||
}
|
||||
|
||||
// Show usage if no arguments
|
||||
if (args.length === 0) {
|
||||
return message.reply(
|
||||
`Usage: ${guildConfig.prefix}role {add|remove} {${guildConfig.allowedRolesForRequest}}`
|
||||
);
|
||||
}
|
||||
|
||||
const action = args[0]?.toLowerCase();
|
||||
const roleName = args.slice(1).join(' ');
|
||||
|
||||
// Validate action
|
||||
if (!['add', 'remove'].includes(action)) {
|
||||
return message.reply(
|
||||
`You must use add/remove after the role command! *e.g. ${guildConfig.prefix}role add <rolename>*`
|
||||
);
|
||||
}
|
||||
|
||||
// Validate role name
|
||||
if (!roleName) {
|
||||
return message.reply(
|
||||
`Usage: ${guildConfig.prefix}role {add|remove} {${guildConfig.allowedRolesForRequest}}`
|
||||
);
|
||||
}
|
||||
|
||||
// Check if role is in the allowed list
|
||||
const allowedRoles = guildConfig.allowedRolesForRequest.split('|');
|
||||
const roleRegex = new RegExp(guildConfig.allowedRolesForRequest, 'i');
|
||||
|
||||
if (!roleRegex.test(roleName)) {
|
||||
return message.reply(
|
||||
`**${roleName}** is not a valid role name! The roles allowed for request are: ${allowedRoles.join(', ')}`
|
||||
);
|
||||
}
|
||||
|
||||
// Find the role in the guild (case-sensitive search)
|
||||
const role = message.guild.roles.cache.find(r =>
|
||||
r.name.toLowerCase() === roleName.toLowerCase()
|
||||
);
|
||||
|
||||
if (!role) {
|
||||
return message.reply(`${roleName} is not a role on this server!`);
|
||||
}
|
||||
|
||||
try {
|
||||
if (action === 'add') {
|
||||
await message.member.roles.add(role, 'User requested');
|
||||
await message.react('👍');
|
||||
console.log(`Added role ${role.name} to ${message.author.username}`);
|
||||
} else if (action === 'remove') {
|
||||
await message.member.roles.remove(role, 'User requested');
|
||||
await message.react('👍');
|
||||
console.log(`Removed role ${role.name} from ${message.author.username}`);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error managing role ${role.name}:`, error);
|
||||
await message.react('⚠️');
|
||||
|
||||
// Send error message if we can't react
|
||||
if (!message.reactions.cache.has('⚠️')) {
|
||||
await message.reply('I encountered an error managing that role. Make sure I have the proper permissions!');
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
87
src/commands/prefix/sfx.js
Normal file
87
src/commands/prefix/sfx.js
Normal file
@@ -0,0 +1,87 @@
|
||||
const axios = require('axios');
|
||||
const { chunkSubstr } = require('../../utils/helpers');
|
||||
const sfxManager = require('../../services/sfxManager');
|
||||
const voiceService = require('../../services/voiceService');
|
||||
|
||||
module.exports = {
|
||||
name: 'sfx',
|
||||
description: 'Play a sound effect',
|
||||
|
||||
async execute(message, args, guildConfig) {
|
||||
// Check if SFX is allowed in this channel
|
||||
if (guildConfig.allowedSfxChannels) {
|
||||
const allowedChannels = new RegExp(guildConfig.allowedSfxChannels);
|
||||
if (!allowedChannels.test(message.channel.name)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const sfxName = args[0];
|
||||
|
||||
// Log the SFX command
|
||||
if (sfxName) {
|
||||
console.log(
|
||||
`SFX '${sfxName}' requested in ${guildConfig.internalName || message.guild.name}#${message.channel.name} from @${message.author.username}`
|
||||
);
|
||||
}
|
||||
|
||||
// If no SFX specified, show the list
|
||||
if (!sfxName) {
|
||||
try {
|
||||
const response = await axios.get('https://rentry.co/ghbotsfx/raw');
|
||||
|
||||
// Break into chunks if message is too long
|
||||
let chunks = [response.data];
|
||||
if (response.data.length > 2000) {
|
||||
chunks = chunkSubstr(response.data, Math.ceil(response.data.length / 2));
|
||||
}
|
||||
|
||||
for (const chunk of chunks) {
|
||||
await message.channel.send(chunk);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching SFX list:', error);
|
||||
await message.reply('Could not fetch the SFX list.');
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if SFX exists
|
||||
if (!sfxManager.hasSFX(sfxName)) {
|
||||
return message.reply('This sound effect does not exist!');
|
||||
}
|
||||
|
||||
// Check if user is in a voice channel
|
||||
if (!message.member.voice.channel) {
|
||||
return message.reply('You need to be in a voice channel to use this command!');
|
||||
}
|
||||
|
||||
try {
|
||||
// Join the voice channel
|
||||
await voiceService.join(message.member.voice.channel);
|
||||
|
||||
// Get the SFX file path
|
||||
const sfxPath = sfxManager.getSFXPath(sfxName);
|
||||
|
||||
// Play the sound effect
|
||||
await voiceService.play(
|
||||
message.guild.id,
|
||||
sfxPath,
|
||||
{
|
||||
volume: guildConfig.sfxVolume || 0.5
|
||||
}
|
||||
);
|
||||
|
||||
// Leave the voice channel after playing
|
||||
setTimeout(() => {
|
||||
voiceService.leave(message.guild.id);
|
||||
}, 500);
|
||||
|
||||
console.log(`✅ Successfully played SFX '${sfxName}'`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Error playing SFX '${sfxName}':`, error);
|
||||
await message.reply("I couldn't play that sound effect. Make sure I have permission to join your voice channel!");
|
||||
}
|
||||
}
|
||||
};
|
||||
77
src/commands/slash/index.js
Normal file
77
src/commands/slash/index.js
Normal file
@@ -0,0 +1,77 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
class SlashCommandHandler {
|
||||
constructor() {
|
||||
this.commands = new Map();
|
||||
this.loadCommands();
|
||||
}
|
||||
|
||||
/**
|
||||
* Load all slash command modules
|
||||
*/
|
||||
loadCommands() {
|
||||
const commandFiles = fs.readdirSync(__dirname)
|
||||
.filter(file => file.endsWith('.js') && file !== 'index.js');
|
||||
|
||||
for (const file of commandFiles) {
|
||||
const command = require(path.join(__dirname, file));
|
||||
|
||||
if (command.data?.name) {
|
||||
this.commands.set(command.data.name, command);
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`Loaded ${this.commands.size} slash commands`);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get slash command definitions for registration
|
||||
* @returns {Array}
|
||||
*/
|
||||
getSlashCommandDefinitions() {
|
||||
return Array.from(this.commands.values()).map(cmd => cmd.data.toJSON());
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a slash command
|
||||
* @param {string} commandName
|
||||
* @param {CommandInteraction} interaction
|
||||
* @param {Object} guildConfig
|
||||
*/
|
||||
async execute(commandName, interaction, guildConfig) {
|
||||
const command = this.commands.get(commandName);
|
||||
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await command.execute(interaction, guildConfig);
|
||||
} catch (error) {
|
||||
console.error(`Error executing slash command ${commandName}:`, error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle autocomplete interactions
|
||||
* @param {AutocompleteInteraction} interaction
|
||||
* @param {Object} guildConfig
|
||||
*/
|
||||
async handleAutocomplete(interaction, guildConfig) {
|
||||
const command = this.commands.get(interaction.commandName);
|
||||
|
||||
if (!command || !command.autocomplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await command.autocomplete(interaction, guildConfig);
|
||||
} catch (error) {
|
||||
console.error(`Error handling autocomplete for ${interaction.commandName}:`, error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new SlashCommandHandler();
|
||||
108
src/commands/slash/sfx.js
Normal file
108
src/commands/slash/sfx.js
Normal file
@@ -0,0 +1,108 @@
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const sfxManager = require('../../services/sfxManager');
|
||||
const voiceService = require('../../services/voiceService');
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('sfx')
|
||||
.setDescription('Play a sound effect')
|
||||
.addStringOption(option =>
|
||||
option.setName('sound')
|
||||
.setDescription('The sound effect to play')
|
||||
.setRequired(true)
|
||||
.setAutocomplete(true)
|
||||
),
|
||||
|
||||
async execute(interaction, guildConfig) {
|
||||
// Check if SFX is allowed in this channel
|
||||
if (guildConfig.allowedSfxChannels) {
|
||||
const allowedChannels = new RegExp(guildConfig.allowedSfxChannels);
|
||||
if (!allowedChannels.test(interaction.channel.name)) {
|
||||
return interaction.reply({
|
||||
content: 'Sound effects are not allowed in this channel!',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const sfxName = interaction.options.getString('sound');
|
||||
|
||||
// Log the slash command SFX request
|
||||
console.log(
|
||||
`/sfx '${sfxName}' requested in ${guildConfig.internalName || interaction.guild.name}#${interaction.channel.name} from @${interaction.user.username}`
|
||||
);
|
||||
|
||||
// Check if SFX exists
|
||||
if (!sfxManager.hasSFX(sfxName)) {
|
||||
return interaction.reply({
|
||||
content: 'This sound effect does not exist!',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user is in a voice channel
|
||||
const member = interaction.member;
|
||||
if (!member.voice.channel) {
|
||||
return interaction.reply({
|
||||
content: 'You need to be in a voice channel to use this command!',
|
||||
ephemeral: true
|
||||
});
|
||||
}
|
||||
|
||||
// Defer the reply as joining voice might take a moment
|
||||
await interaction.deferReply();
|
||||
|
||||
try {
|
||||
// Join the voice channel
|
||||
await voiceService.join(member.voice.channel);
|
||||
|
||||
// Get the SFX file path
|
||||
const sfxPath = sfxManager.getSFXPath(sfxName);
|
||||
|
||||
// Play the sound effect
|
||||
await voiceService.play(
|
||||
interaction.guild.id,
|
||||
sfxPath,
|
||||
{
|
||||
volume: guildConfig.sfxVolume || 0.5
|
||||
}
|
||||
);
|
||||
|
||||
// Update the reply
|
||||
await interaction.editReply(`Playing sound effect: **${sfxName}**`);
|
||||
|
||||
// Leave the voice channel after playing
|
||||
setTimeout(() => {
|
||||
voiceService.leave(interaction.guild.id);
|
||||
}, 500);
|
||||
|
||||
console.log(`✅ Successfully played /sfx '${sfxName}'`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Error playing /sfx '${sfxName}':`, error);
|
||||
await interaction.editReply({
|
||||
content: "I couldn't play that sound effect. Make sure I have permission to join your voice channel!"
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
async autocomplete(interaction, guildConfig) {
|
||||
const focusedValue = interaction.options.getFocused().toLowerCase();
|
||||
|
||||
// Get all SFX names
|
||||
const choices = sfxManager.getSFXNames();
|
||||
|
||||
// Filter based on what the user has typed
|
||||
const filtered = choices
|
||||
.filter(choice => choice.toLowerCase().includes(focusedValue))
|
||||
.slice(0, 25); // Discord limits autocomplete to 25 choices
|
||||
|
||||
// Respond with the filtered choices
|
||||
await interaction.respond(
|
||||
filtered.map(choice => ({
|
||||
name: choice,
|
||||
value: choice
|
||||
}))
|
||||
);
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user