CHECKPOINT: Interactive soundboard and refactored SFX system
Major Features Implemented: - Complete Discord.js v14 modernization from v12 with hybrid command system - SQLite database for dynamic guild configuration management - Interactive soundboard with categorized button interface (/soundboard) - Three-tier SFX interface: prefix (!sfx), autocomplete (/sfx), and visual soundboard - Auto-registration system for public bot distribution - Soft delete guild management preserving configurations Technical Improvements: - Refactored SFX playing into reusable service methods (playSFXInteraction/playSFXMessage) - Smart markdown chunking that respects code block boundaries - High-performance caching for 275+ sound effects with autocomplete optimization - Modern Discord.js v14 patterns (MessageFlags.Ephemeral, proper intents) - Fixed security vulnerability in @discordjs/opus with pnpm overrides - Docker deployment with Node 20 and npm for reliable SQLite compilation Interactive Soundboard Features: - Category-based navigation with buttons (GENERAL, NERDS, TWIN PEAKS, etc.) - Pagination support for large categories (16 sounds per page, 4 per row) - Real-time status updates (Playing → Finished playing) - Navigation buttons (Previous/Next/Back to Categories) - Ephemeral responses for clean chat experience Database System: - Auto-migration from config.json to SQLite on first run - /config slash commands for live server configuration - Scheduled events with timezone support (object and cron formats) - Guild auto-registration with welcome messages for new servers Current State: Fully functional modern Discord bot ready for public distribution
This commit is contained in:
@@ -50,13 +50,6 @@ module.exports = {
|
||||
}
|
||||
|
||||
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) {
|
||||
@@ -92,42 +85,12 @@ module.exports = {
|
||||
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!");
|
||||
}
|
||||
// Use the reusable SFX playing method for messages
|
||||
await sfxManager.playSFXMessage(message, sfxName, guildConfig);
|
||||
}
|
||||
};
|
||||
@@ -1,5 +1,6 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const { MessageFlags } = require("discord.js");
|
||||
|
||||
class SlashCommandHandler {
|
||||
constructor() {
|
||||
@@ -11,12 +12,13 @@ class SlashCommandHandler {
|
||||
* Load all slash command modules
|
||||
*/
|
||||
loadCommands() {
|
||||
const commandFiles = fs.readdirSync(__dirname)
|
||||
.filter(file => file.endsWith('.js') && file !== 'index.js');
|
||||
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);
|
||||
}
|
||||
@@ -30,18 +32,18 @@ class SlashCommandHandler {
|
||||
* @returns {Array}
|
||||
*/
|
||||
getSlashCommandDefinitions() {
|
||||
return Array.from(this.commands.values()).map(cmd => cmd.data.toJSON());
|
||||
return Array.from(this.commands.values()).map((cmd) => cmd.data.toJSON());
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute a slash command
|
||||
* @param {string} commandName
|
||||
* @param {CommandInteraction} interaction
|
||||
* @param {Object} guildConfig
|
||||
* @param {string} commandName
|
||||
* @param {CommandInteraction} interaction
|
||||
* @param {Object} guildConfig
|
||||
*/
|
||||
async execute(commandName, interaction, guildConfig) {
|
||||
const command = this.commands.get(commandName);
|
||||
|
||||
|
||||
if (!command) {
|
||||
return;
|
||||
}
|
||||
@@ -56,12 +58,12 @@ class SlashCommandHandler {
|
||||
|
||||
/**
|
||||
* Handle autocomplete interactions
|
||||
* @param {AutocompleteInteraction} interaction
|
||||
* @param {Object} guildConfig
|
||||
* @param {AutocompleteInteraction} interaction
|
||||
* @param {Object} guildConfig
|
||||
*/
|
||||
async handleAutocomplete(interaction, guildConfig) {
|
||||
const command = this.commands.get(interaction.commandName);
|
||||
|
||||
|
||||
if (!command || !command.autocomplete) {
|
||||
return;
|
||||
}
|
||||
@@ -69,9 +71,48 @@ class SlashCommandHandler {
|
||||
try {
|
||||
await command.autocomplete(interaction, guildConfig);
|
||||
} catch (error) {
|
||||
console.error(`Error handling autocomplete for ${interaction.commandName}:`, error);
|
||||
console.error(
|
||||
`Error handling autocomplete for ${interaction.commandName}:`,
|
||||
error
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle button interactions for soundboard
|
||||
* @param {ButtonInteraction} interaction
|
||||
* @param {Object} guildConfig
|
||||
*/
|
||||
async handleButton(interaction, guildConfig) {
|
||||
if (interaction.customId.startsWith("soundboard_")) {
|
||||
const soundboardCommand = this.commands.get("soundboard");
|
||||
|
||||
if (!soundboardCommand) return;
|
||||
|
||||
try {
|
||||
if (interaction.customId.startsWith("soundboard_category_")) {
|
||||
await soundboardCommand.handleCategorySelection(
|
||||
interaction,
|
||||
guildConfig
|
||||
);
|
||||
} else if (interaction.customId.startsWith("soundboard_play_")) {
|
||||
await soundboardCommand.handleSoundPlay(interaction, guildConfig);
|
||||
} else if (interaction.customId === "soundboard_back") {
|
||||
// Re-execute the main soundboard command
|
||||
await soundboardCommand.execute(interaction, guildConfig);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error handling soundboard button interaction:`, error);
|
||||
|
||||
if (!interaction.replied && !interaction.deferred) {
|
||||
await interaction.reply({
|
||||
content: "There was an error with the soundboard!",
|
||||
flags: [MessageFlags.Ephemeral],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new SlashCommandHandler();
|
||||
module.exports = new SlashCommandHandler();
|
||||
|
||||
@@ -26,64 +26,17 @@ module.exports = {
|
||||
}
|
||||
|
||||
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!',
|
||||
flags: [MessageFlags.Ephemeral]
|
||||
});
|
||||
}
|
||||
|
||||
// Check if user is in a voice channel
|
||||
const member = interaction.member;
|
||||
if (!member.voice.channel) {
|
||||
if (!interaction.member.voice.channel) {
|
||||
return interaction.reply({
|
||||
content: 'You need to be in a voice channel to use this command!',
|
||||
flags: [MessageFlags.Ephemeral]
|
||||
});
|
||||
}
|
||||
|
||||
// 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!"
|
||||
});
|
||||
}
|
||||
// Use the reusable SFX playing method
|
||||
await sfxManager.playSFXInteraction(interaction, sfxName, guildConfig, 'slash');
|
||||
},
|
||||
|
||||
async autocomplete(interaction, guildConfig) {
|
||||
|
||||
258
src/commands/slash/soundboard.js
Normal file
258
src/commands/slash/soundboard.js
Normal file
@@ -0,0 +1,258 @@
|
||||
const {
|
||||
SlashCommandBuilder,
|
||||
ActionRowBuilder,
|
||||
ButtonBuilder,
|
||||
ButtonStyle,
|
||||
EmbedBuilder,
|
||||
MessageFlags,
|
||||
} = require("discord.js");
|
||||
const fs = require("fs");
|
||||
const path = require("path");
|
||||
const sfxManager = require("../../services/sfxManager");
|
||||
const voiceService = require("../../services/voiceService");
|
||||
|
||||
// Parse categories from README.md
|
||||
function getSFXCategories() {
|
||||
try {
|
||||
const sfxReadmePath = path.join(
|
||||
__dirname,
|
||||
"..",
|
||||
"..",
|
||||
"..",
|
||||
"sfx",
|
||||
"README.md"
|
||||
);
|
||||
|
||||
if (!fs.existsSync(sfxReadmePath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(sfxReadmePath, "utf-8");
|
||||
const categories = {};
|
||||
|
||||
// Parse categories and their sounds
|
||||
const lines = content.split("\n");
|
||||
let currentCategory = null;
|
||||
|
||||
for (const line of lines) {
|
||||
const headerMatch = line.match(/^\*\*([^*]+)\*\*$/);
|
||||
if (headerMatch) {
|
||||
currentCategory = headerMatch[1];
|
||||
categories[currentCategory] = [];
|
||||
} else if (currentCategory && line.trim() && !line.startsWith("```")) {
|
||||
// Parse comma-separated sounds from the line
|
||||
const sounds = line
|
||||
.split(",")
|
||||
.map((s) => s.trim())
|
||||
.filter((s) => s.length > 0);
|
||||
categories[currentCategory].push(...sounds);
|
||||
}
|
||||
}
|
||||
|
||||
return categories;
|
||||
} catch (error) {
|
||||
console.error("Error parsing SFX categories:", error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName("soundboard")
|
||||
.setDescription("Interactive soundboard with categorized buttons"),
|
||||
|
||||
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!",
|
||||
flags: [MessageFlags.Ephemeral],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Check if user is in a voice channel
|
||||
if (!interaction.member.voice.channel) {
|
||||
return interaction.reply({
|
||||
content: "You need to be in a voice channel to use the soundboard!",
|
||||
flags: [MessageFlags.Ephemeral],
|
||||
});
|
||||
}
|
||||
|
||||
const categories = getSFXCategories();
|
||||
|
||||
if (!categories) {
|
||||
return interaction.reply({
|
||||
content:
|
||||
"Soundboard not available - SFX categories could not be loaded.",
|
||||
flags: [MessageFlags.Ephemeral],
|
||||
});
|
||||
}
|
||||
|
||||
// Create category selection buttons (max 5 buttons per row, 5 rows max = 25 buttons total)
|
||||
const categoryNames = Object.keys(categories);
|
||||
const rows = [];
|
||||
let buttonCount = 0;
|
||||
|
||||
for (let i = 0; i < categoryNames.length && rows.length < 5; i += 5) {
|
||||
const row = new ActionRowBuilder();
|
||||
const categoriesInRow = categoryNames.slice(i, i + 5);
|
||||
|
||||
for (const category of categoriesInRow) {
|
||||
if (buttonCount >= 25) break; // Discord limit is 25 components total
|
||||
|
||||
const button = new ButtonBuilder()
|
||||
.setCustomId(
|
||||
`soundboard_category_${category.toLowerCase().replace(/\s+/g, "_")}`
|
||||
)
|
||||
.setLabel(category.length > 80 ? category.substring(0, 77) + '...' : category)
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setEmoji("🎵");
|
||||
|
||||
row.addComponents(button);
|
||||
buttonCount++;
|
||||
}
|
||||
|
||||
if (row.components.length > 0) {
|
||||
rows.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle("🎛️ Interactive Soundboard")
|
||||
.setDescription("Choose a category to browse sound effects:")
|
||||
.setColor(0x21c629)
|
||||
.addFields([
|
||||
{
|
||||
name: "Available Categories",
|
||||
value: categoryNames
|
||||
.map((cat) => `🎵 **${cat}** (${categories[cat].length} sounds)`)
|
||||
.join("\n"),
|
||||
inline: false,
|
||||
},
|
||||
])
|
||||
.setFooter({ text: "Click a category button to browse sounds" });
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [embed],
|
||||
components: rows,
|
||||
});
|
||||
},
|
||||
|
||||
async handleCategorySelection(interaction, guildConfig) {
|
||||
const customId = interaction.customId;
|
||||
let categoryKey, page = 0;
|
||||
|
||||
if (customId.includes('_page_')) {
|
||||
// Handle pagination: soundboard_category_general_page_1
|
||||
const parts = customId.replace("soundboard_category_", "").split('_page_');
|
||||
categoryKey = parts[0].replace(/_/g, " ").toUpperCase();
|
||||
page = parseInt(parts[1]) || 0;
|
||||
} else {
|
||||
// Handle initial category selection
|
||||
categoryKey = customId
|
||||
.replace("soundboard_category_", "")
|
||||
.replace(/_/g, " ")
|
||||
.toUpperCase();
|
||||
}
|
||||
|
||||
const categories = getSFXCategories();
|
||||
|
||||
if (!categories || !categories[categoryKey]) {
|
||||
return interaction.reply({
|
||||
content: "Category not found!",
|
||||
flags: [MessageFlags.Ephemeral],
|
||||
});
|
||||
}
|
||||
|
||||
const allSounds = categories[categoryKey].filter(sound => sfxManager.hasSFX(sound));
|
||||
const soundsPerPage = 16; // 4 sounds per row × 4 rows = 16 sounds per page
|
||||
const totalPages = Math.ceil(allSounds.length / soundsPerPage);
|
||||
const startIndex = page * soundsPerPage;
|
||||
const sounds = allSounds.slice(startIndex, startIndex + soundsPerPage);
|
||||
|
||||
const rows = [];
|
||||
let buttonCount = 0;
|
||||
|
||||
// Create sound buttons (4 per row, 4 rows for sounds + 1 for navigation = 16 sound buttons max)
|
||||
for (let i = 0; i < sounds.length && rows.length < 4; i += 4) {
|
||||
const row = new ActionRowBuilder();
|
||||
const soundsInRow = sounds.slice(i, i + 4);
|
||||
|
||||
for (const sound of soundsInRow) {
|
||||
if (buttonCount >= 16) break; // Leave room for navigation row
|
||||
|
||||
const button = new ButtonBuilder()
|
||||
.setCustomId(`soundboard_play_${sound}`)
|
||||
.setLabel(sound.length > 80 ? sound.substring(0, 77) + '...' : sound)
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setEmoji("▶️");
|
||||
|
||||
row.addComponents(button);
|
||||
buttonCount++;
|
||||
}
|
||||
|
||||
if (row.components.length > 0) {
|
||||
rows.push(row);
|
||||
}
|
||||
}
|
||||
|
||||
// Add navigation row with back button and pagination if needed
|
||||
const navRow = new ActionRowBuilder();
|
||||
|
||||
// Add previous page button if not on first page
|
||||
if (page > 0) {
|
||||
const prevButton = new ButtonBuilder()
|
||||
.setCustomId(`soundboard_category_${categoryKey.toLowerCase().replace(/\s+/g, "_")}_page_${page - 1}`)
|
||||
.setLabel("« Previous")
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setEmoji("◀️");
|
||||
navRow.addComponents(prevButton);
|
||||
}
|
||||
|
||||
// Add back to categories button
|
||||
const backButton = new ButtonBuilder()
|
||||
.setCustomId("soundboard_back")
|
||||
.setLabel("Back to Categories")
|
||||
.setStyle(ButtonStyle.Primary)
|
||||
.setEmoji("🔙");
|
||||
navRow.addComponents(backButton);
|
||||
|
||||
// Add next page button if there are more pages
|
||||
if (page < totalPages - 1) {
|
||||
const nextButton = new ButtonBuilder()
|
||||
.setCustomId(`soundboard_category_${categoryKey.toLowerCase().replace(/\s+/g, "_")}_page_${page + 1}`)
|
||||
.setLabel("Next »")
|
||||
.setStyle(ButtonStyle.Secondary)
|
||||
.setEmoji("▶️");
|
||||
navRow.addComponents(nextButton);
|
||||
}
|
||||
|
||||
rows.push(navRow);
|
||||
|
||||
// Show pagination info
|
||||
const paginationNote = totalPages > 1 ? `\n\n*Page ${page + 1} of ${totalPages} (${allSounds.length} total sounds)*` : '';
|
||||
|
||||
const embed = new EmbedBuilder()
|
||||
.setTitle(`🎵 ${categoryKey} Soundboard`)
|
||||
.setDescription(
|
||||
`Choose a sound effect to play:${paginationNote}`
|
||||
)
|
||||
.setColor(0x21c629)
|
||||
.setFooter({ text: "Click a sound button to play it" });
|
||||
|
||||
await interaction.update({
|
||||
embeds: [embed],
|
||||
components: rows,
|
||||
});
|
||||
},
|
||||
|
||||
async handleSoundPlay(interaction, guildConfig) {
|
||||
const soundName = interaction.customId.replace("soundboard_play_", "");
|
||||
|
||||
// Use the reusable SFX playing method
|
||||
await sfxManager.playSFXInteraction(interaction, soundName, guildConfig, 'soundboard');
|
||||
},
|
||||
};
|
||||
@@ -176,9 +176,9 @@ client.on(Events.MessageCreate, async (message) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Interaction handler for slash commands
|
||||
// Interaction handler for slash commands and components
|
||||
client.on(Events.InteractionCreate, async (interaction) => {
|
||||
if (!interaction.isChatInputCommand() && !interaction.isAutocomplete())
|
||||
if (!interaction.isChatInputCommand() && !interaction.isAutocomplete() && !interaction.isButton())
|
||||
return;
|
||||
|
||||
// Get guild configuration from database/file
|
||||
@@ -194,6 +194,8 @@ client.on(Events.InteractionCreate, async (interaction) => {
|
||||
interaction,
|
||||
guildConfig
|
||||
);
|
||||
} else if (interaction.isButton()) {
|
||||
await slashCommands.handleButton(interaction, guildConfig);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error handling interaction:", error);
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
const { MessageFlags } = require('discord.js');
|
||||
const voiceService = require('./voiceService');
|
||||
|
||||
class SFXManager {
|
||||
constructor() {
|
||||
@@ -130,6 +132,118 @@ class SFXManager {
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a sound effect via interaction (slash commands and soundboard)
|
||||
* @param {Object} interaction - Discord interaction object
|
||||
* @param {string} sfxName - Name of the sound effect to play
|
||||
* @param {Object} guildConfig - Guild configuration
|
||||
* @param {string} commandType - Type of command ('slash' or 'soundboard')
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async playSFXInteraction(interaction, sfxName, guildConfig, commandType = 'slash') {
|
||||
// Log the request
|
||||
const logPrefix = commandType === 'soundboard' ? 'Soundboard' : '/sfx';
|
||||
console.log(
|
||||
`${logPrefix} '${sfxName}' requested in ${guildConfig.internalName || interaction.guild.name}#${interaction.channel.name} from @${interaction.user.username}`
|
||||
);
|
||||
|
||||
// Check if SFX exists
|
||||
if (!this.hasSFX(sfxName)) {
|
||||
await interaction.reply({
|
||||
content: `❌ This sound effect does not exist!`,
|
||||
flags: [MessageFlags.Ephemeral]
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Immediately reply with playing status
|
||||
await interaction.reply({
|
||||
content: `🔊 Playing: **${sfxName}**`,
|
||||
flags: [MessageFlags.Ephemeral]
|
||||
});
|
||||
|
||||
// Join the voice channel
|
||||
await voiceService.join(interaction.member.voice.channel);
|
||||
|
||||
// Get the SFX file path and play
|
||||
const sfxPath = this.getSFXPath(sfxName);
|
||||
await voiceService.play(interaction.guild.id, sfxPath, {
|
||||
volume: guildConfig.sfxVolume || 0.5,
|
||||
});
|
||||
|
||||
// Update the interaction to show completion
|
||||
try {
|
||||
await interaction.editReply({
|
||||
content: `✅ Finished playing: **${sfxName}**`
|
||||
});
|
||||
} catch (editError) {
|
||||
console.error('Error updating interaction with completion message:', editError);
|
||||
}
|
||||
|
||||
// Leave the voice channel after playing
|
||||
setTimeout(() => {
|
||||
voiceService.leave(interaction.guild.id);
|
||||
}, 500);
|
||||
|
||||
console.log(`✅ Successfully played ${logPrefix.toLowerCase()} '${sfxName}'`);
|
||||
|
||||
} catch (error) {
|
||||
console.error(`❌ Error playing ${logPrefix.toLowerCase()} '${sfxName}':`, error);
|
||||
|
||||
// Update the reply with error message
|
||||
try {
|
||||
await interaction.editReply({
|
||||
content: "❌ Couldn't play that sound effect. Make sure I have permission to join your voice channel!"
|
||||
});
|
||||
} catch (editError) {
|
||||
console.error('Error updating interaction with error message:', editError);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Play a sound effect via message (prefix commands)
|
||||
* @param {Object} message - Discord message object
|
||||
* @param {string} sfxName - Name of the sound effect to play
|
||||
* @param {Object} guildConfig - Guild configuration
|
||||
* @returns {Promise<void>}
|
||||
*/
|
||||
async playSFXMessage(message, sfxName, guildConfig) {
|
||||
// Log the request
|
||||
console.log(
|
||||
`SFX '${sfxName}' requested in ${guildConfig.internalName || message.guild.name}#${message.channel.name} from @${message.author.username}`
|
||||
);
|
||||
|
||||
// Check if SFX exists
|
||||
if (!this.hasSFX(sfxName)) {
|
||||
await message.reply('❌ This sound effect does not exist!');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Join the voice channel
|
||||
await voiceService.join(message.member.voice.channel);
|
||||
|
||||
// Get the SFX file path and play
|
||||
const sfxPath = this.getSFXPath(sfxName);
|
||||
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("❌ Couldn't play that sound effect. Make sure I have permission to join your voice channel!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
module.exports = new SFXManager();
|
||||
Reference in New Issue
Block a user