Compare commits
7 Commits
feat/role-
...
dependabot
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b8d2948953 | ||
|
|
2e6cfe21d8 | ||
|
|
5d72159cb2 | ||
|
|
8823eac094 | ||
|
|
6d93f3dcad | ||
|
|
7dc7a92dd1 | ||
|
|
b2821d412c |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -54,8 +54,8 @@ data/
|
|||||||
start.bat
|
start.bat
|
||||||
tokens.json
|
tokens.json
|
||||||
|
|
||||||
config.*json
|
seed.json
|
||||||
!config.example.json
|
!seed.example.json
|
||||||
.env
|
.env
|
||||||
|
|
||||||
*.todo
|
*.todo
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
# Use Node 20 LTS with full Debian for better compatibility
|
# Use Node 20 LTS with full Debian for better compatibility
|
||||||
FROM node:20
|
FROM node:20
|
||||||
|
|
||||||
|
RUN apt update && apt install -y sqlite3
|
||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# Copy package files (npm will work better for native modules in Docker)
|
# Copy package files (npm will work better for native modules in Docker)
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ services:
|
|||||||
container_name: discord-bot
|
container_name: discord-bot
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
volumes:
|
volumes:
|
||||||
# Configuration files (read-only)
|
# Seed file for initial database population (read-only)
|
||||||
- ./config.json:/app/config.json:ro
|
- ./seed.json:/app/seed.json:ro
|
||||||
- ./conf:/app/conf:ro
|
- ./conf:/app/conf:ro
|
||||||
|
|
||||||
# Sound effects directory (read-only)
|
# Sound effects directory (read-only)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@discordjs/opus": "^0.9.0",
|
"@discordjs/opus": "^0.9.0",
|
||||||
"@discordjs/voice": "^0.18.0",
|
"@discordjs/voice": "^0.18.0",
|
||||||
"axios": "^1.11.0",
|
"axios": "^1.12.0",
|
||||||
"better-sqlite3": "^11.10.0",
|
"better-sqlite3": "^11.10.0",
|
||||||
"discord.js": "^14.21.0",
|
"discord.js": "^14.21.0",
|
||||||
"ffmpeg-static": "^5.2.0",
|
"ffmpeg-static": "^5.2.0",
|
||||||
@@ -32,7 +32,7 @@
|
|||||||
"restart": "docker compose restart",
|
"restart": "docker compose restart",
|
||||||
"logs": "docker compose logs -f",
|
"logs": "docker compose logs -f",
|
||||||
"boom": "pnpm stop && pnpm build && pnpm start",
|
"boom": "pnpm stop && pnpm build && pnpm start",
|
||||||
"reset-db": "pnpm stop && rm -f data/ghbot.db data/ghbot.db-shm data/ghbot.db-wal && echo 'Database reset complete. Run pnpm start to re-seed from config.json'",
|
"reset-db": "pnpm stop && rm -f data/*.db* && echo 'Database reset complete. Run pnpm start to re-seed from seed.json'",
|
||||||
"image:build": "docker build -t ghbot:${VERSION:-latest} .",
|
"image:build": "docker build -t ghbot:${VERSION:-latest} .",
|
||||||
"image:run": "docker run -d --name ghbot --restart always ghbot:${VERSION:-latest}"
|
"image:run": "docker run -d --name ghbot --restart always ghbot:${VERSION:-latest}"
|
||||||
},
|
},
|
||||||
|
|||||||
8
pnpm-lock.yaml
generated
8
pnpm-lock.yaml
generated
@@ -15,8 +15,8 @@ dependencies:
|
|||||||
specifier: ^0.18.0
|
specifier: ^0.18.0
|
||||||
version: 0.18.0(ffmpeg-static@5.2.0)(opusscript@0.1.1)(opusscript@0.1.1)
|
version: 0.18.0(ffmpeg-static@5.2.0)(opusscript@0.1.1)(opusscript@0.1.1)
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.11.0
|
specifier: ^1.12.0
|
||||||
version: 1.11.0
|
version: 1.12.0
|
||||||
better-sqlite3:
|
better-sqlite3:
|
||||||
specifier: ^11.10.0
|
specifier: ^11.10.0
|
||||||
version: 11.10.0
|
version: 11.10.0
|
||||||
@@ -201,8 +201,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
resolution: {integrity: sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==}
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
/axios@1.11.0:
|
/axios@1.12.0:
|
||||||
resolution: {integrity: sha512-1Lx3WLFQWm3ooKDYZD1eXmoGO9fxYQjrycfHFC8P0sCfQVXyROp0p9PFWBehewBOdCwHc+f/b8I0fMto5eSfwA==}
|
resolution: {integrity: sha512-oXTDccv8PcfjZmPGlWsPSwtOJCZ/b6W5jAMCNcfwJbCzDckwG0jrYJFaWH1yvivfCXjVzV/SPDEhMB3Q+DSurg==}
|
||||||
dependencies:
|
dependencies:
|
||||||
follow-redirects: 1.15.11
|
follow-redirects: 1.15.11
|
||||||
form-data: 4.0.4
|
form-data: 4.0.4
|
||||||
|
|||||||
@@ -1,11 +1,8 @@
|
|||||||
const axios = require('axios');
|
const sfxManager = require("../../services/sfxManager");
|
||||||
const { chunkSubstr } = require('../../utils/helpers');
|
|
||||||
const sfxManager = require('../../services/sfxManager');
|
|
||||||
const voiceService = require('../../services/voiceService');
|
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
name: 'sfx',
|
name: "sfx",
|
||||||
description: 'Play a sound effect',
|
description: "Play a sound effect",
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Smart chunking that respects markdown block boundaries
|
* Smart chunking that respects markdown block boundaries
|
||||||
@@ -17,7 +14,7 @@ module.exports = {
|
|||||||
const chunks = [];
|
const chunks = [];
|
||||||
const sections = content.split(/(\*\*[^*]+\*\*)/); // Split on headers while keeping them
|
const sections = content.split(/(\*\*[^*]+\*\*)/); // Split on headers while keeping them
|
||||||
|
|
||||||
let currentChunk = '';
|
let currentChunk = "";
|
||||||
|
|
||||||
for (const section of sections) {
|
for (const section of sections) {
|
||||||
// If adding this section would exceed the limit
|
// If adding this section would exceed the limit
|
||||||
@@ -46,12 +43,19 @@ module.exports = {
|
|||||||
// If no SFX specified, show the list
|
// If no SFX specified, show the list
|
||||||
if (!sfxName) {
|
if (!sfxName) {
|
||||||
try {
|
try {
|
||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const sfxReadmePath = path.join(__dirname, '..', '..', '..', 'sfx', 'README.md');
|
const sfxReadmePath = path.join(
|
||||||
|
__dirname,
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"..",
|
||||||
|
"sfx",
|
||||||
|
"README.md"
|
||||||
|
);
|
||||||
|
|
||||||
if (fs.existsSync(sfxReadmePath)) {
|
if (fs.existsSync(sfxReadmePath)) {
|
||||||
const sfxListContent = fs.readFileSync(sfxReadmePath, 'utf-8');
|
const sfxListContent = fs.readFileSync(sfxReadmePath, "utf-8");
|
||||||
|
|
||||||
// Break into chunks if too long (Discord limit is 2000 characters)
|
// Break into chunks if too long (Discord limit is 2000 characters)
|
||||||
if (sfxListContent.length <= 2000) {
|
if (sfxListContent.length <= 2000) {
|
||||||
@@ -66,23 +70,27 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Fallback to generated list if README doesn't exist
|
// Fallback to generated list if README doesn't exist
|
||||||
const sfxNames = sfxManager.getSFXNames();
|
const sfxNames = sfxManager.getSfxNames();
|
||||||
const sfxList = `**Available Sound Effects (${sfxNames.length}):**\n\`\`\`\n${sfxNames.join(', ')}\n\`\`\``;
|
const sfxList = `**Available Sound Effects (${
|
||||||
|
sfxNames.length
|
||||||
|
}):**\n\`\`\`\n${sfxNames.join(", ")}\n\`\`\``;
|
||||||
await message.channel.send(sfxList);
|
await message.channel.send(sfxList);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error reading SFX list:', error);
|
console.error("Error reading SFX list:", error);
|
||||||
await message.reply('Could not load the SFX list.');
|
await message.reply("Could not load the SFX list.");
|
||||||
}
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if user is in a voice channel
|
// Check if user is in a voice channel
|
||||||
if (!message.member.voice.channel) {
|
if (!message.member.voice.channel) {
|
||||||
return message.reply('You need to be in a voice channel to use this command!');
|
return message.reply(
|
||||||
|
"You need to be in a voice channel to use this command!"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the reusable SFX playing method for messages
|
// Use the reusable SFX playing method for messages
|
||||||
await sfxManager.playSFXMessage(message, sfxName, guildConfig);
|
await sfxManager.playSfxMessage(message, sfxName, guildConfig);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
@@ -1,50 +1,56 @@
|
|||||||
const { SlashCommandBuilder, MessageFlags } = require('discord.js');
|
const { SlashCommandBuilder, MessageFlags } = require("discord.js");
|
||||||
const sfxManager = require('../../services/sfxManager');
|
const sfxManager = require("../../services/sfxManager");
|
||||||
const voiceService = require('../../services/voiceService');
|
const voiceService = require("../../services/voiceService");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName('sfx')
|
.setName("sfx")
|
||||||
.setDescription('Play a sound effect')
|
.setDescription("Play a sound effect")
|
||||||
.addStringOption(option =>
|
.addStringOption((option) =>
|
||||||
option.setName('sound')
|
option
|
||||||
.setDescription('The sound effect to play')
|
.setName("sound")
|
||||||
|
.setDescription("The sound effect to play")
|
||||||
.setRequired(true)
|
.setRequired(true)
|
||||||
.setAutocomplete(true)
|
.setAutocomplete(true)
|
||||||
),
|
),
|
||||||
|
|
||||||
async execute(interaction, guildConfig) {
|
async execute(interaction, guildConfig) {
|
||||||
const sfxName = interaction.options.getString('sound');
|
const sfxName = interaction.options.getString("sound");
|
||||||
|
|
||||||
// Check if user is in a voice channel
|
// Check if user is in a voice channel
|
||||||
if (!interaction.member.voice.channel) {
|
if (!interaction.member.voice.channel) {
|
||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
content: 'You need to be in a voice channel to use this command!',
|
content: "You need to be in a voice channel to use this command!",
|
||||||
flags: [MessageFlags.Ephemeral]
|
flags: [MessageFlags.Ephemeral],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Use the reusable SFX playing method
|
// Use the reusable SFX playing method
|
||||||
await sfxManager.playSFXInteraction(interaction, sfxName, guildConfig, 'slash');
|
await sfxManager.playSfxInteraction(
|
||||||
|
interaction,
|
||||||
|
sfxName,
|
||||||
|
guildConfig,
|
||||||
|
"slash"
|
||||||
|
);
|
||||||
},
|
},
|
||||||
|
|
||||||
async autocomplete(interaction, guildConfig) {
|
async autocomplete(interaction, guildConfig) {
|
||||||
const focusedValue = interaction.options.getFocused().toLowerCase();
|
const focusedValue = interaction.options.getFocused().toLowerCase();
|
||||||
|
|
||||||
// Get all SFX names
|
// Get all SFX names
|
||||||
const choices = sfxManager.getSFXNames();
|
const choices = sfxManager.getSfxNames();
|
||||||
|
|
||||||
// Filter based on what the user has typed
|
// Filter based on what the user has typed
|
||||||
const filtered = choices
|
const filtered = choices
|
||||||
.filter(choice => choice.toLowerCase().includes(focusedValue))
|
.filter((choice) => choice.toLowerCase().includes(focusedValue))
|
||||||
.slice(0, 25); // Discord limits autocomplete to 25 choices
|
.slice(0, 25); // Discord limits autocomplete to 25 choices
|
||||||
|
|
||||||
// Respond with the filtered choices
|
// Respond with the filtered choices
|
||||||
await interaction.respond(
|
await interaction.respond(
|
||||||
filtered.map(choice => ({
|
filtered.map((choice) => ({
|
||||||
name: choice,
|
name: choice,
|
||||||
value: choice
|
value: choice,
|
||||||
}))
|
}))
|
||||||
);
|
);
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
@@ -9,10 +9,9 @@ const {
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
const sfxManager = require("../../services/sfxManager");
|
const sfxManager = require("../../services/sfxManager");
|
||||||
const voiceService = require("../../services/voiceService");
|
|
||||||
|
|
||||||
// Parse categories from README.md
|
// Parse categories from README.md
|
||||||
function getSFXCategories() {
|
function getSfxCategories() {
|
||||||
try {
|
try {
|
||||||
const sfxReadmePath = path.join(
|
const sfxReadmePath = path.join(
|
||||||
__dirname,
|
__dirname,
|
||||||
@@ -58,7 +57,7 @@ function getSFXCategories() {
|
|||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
.setName("soundboard")
|
.setName("sfxboard")
|
||||||
.setDescription("Interactive soundboard with categorized buttons"),
|
.setDescription("Interactive soundboard with categorized buttons"),
|
||||||
|
|
||||||
async execute(interaction, guildConfig) {
|
async execute(interaction, guildConfig) {
|
||||||
@@ -70,7 +69,7 @@ module.exports = {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const categories = getSFXCategories();
|
const categories = getSfxCategories();
|
||||||
|
|
||||||
if (!categories) {
|
if (!categories) {
|
||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
@@ -112,8 +111,7 @@ module.exports = {
|
|||||||
const embed = new EmbedBuilder()
|
const embed = new EmbedBuilder()
|
||||||
.setTitle("🎛️ Interactive Soundboard")
|
.setTitle("🎛️ Interactive Soundboard")
|
||||||
.setDescription("Choose a category to browse sound effects:")
|
.setDescription("Choose a category to browse sound effects:")
|
||||||
.setColor(0x21c629)
|
.setColor(0x21c629);
|
||||||
.setFooter({ text: "Click a category button to browse sounds" });
|
|
||||||
|
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
embeds: [embed],
|
embeds: [embed],
|
||||||
@@ -145,7 +143,7 @@ module.exports = {
|
|||||||
.toUpperCase();
|
.toUpperCase();
|
||||||
}
|
}
|
||||||
|
|
||||||
const categories = getSFXCategories();
|
const categories = getSfxCategories();
|
||||||
|
|
||||||
if (!categories || !categories[categoryKey]) {
|
if (!categories || !categories[categoryKey]) {
|
||||||
return interaction.reply({
|
return interaction.reply({
|
||||||
@@ -155,7 +153,7 @@ module.exports = {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const allSounds = categories[categoryKey].filter((sound) =>
|
const allSounds = categories[categoryKey].filter((sound) =>
|
||||||
sfxManager.hasSFX(sound)
|
sfxManager.hasSfx(sound)
|
||||||
);
|
);
|
||||||
const soundsPerPage = 16; // 4 sounds per row × 4 rows = 16 sounds per page
|
const soundsPerPage = 16; // 4 sounds per row × 4 rows = 16 sounds per page
|
||||||
const totalPages = Math.ceil(allSounds.length / soundsPerPage);
|
const totalPages = Math.ceil(allSounds.length / soundsPerPage);
|
||||||
@@ -252,7 +250,7 @@ module.exports = {
|
|||||||
const soundName = interaction.customId.replace("soundboard_play_", "");
|
const soundName = interaction.customId.replace("soundboard_play_", "");
|
||||||
|
|
||||||
// Use the reusable SFX playing method
|
// Use the reusable SFX playing method
|
||||||
await sfxManager.playSFXInteraction(
|
await sfxManager.playSfxInteraction(
|
||||||
interaction,
|
interaction,
|
||||||
soundName,
|
soundName,
|
||||||
guildConfig,
|
guildConfig,
|
||||||
|
|||||||
@@ -1,41 +1,9 @@
|
|||||||
const fs = require("fs");
|
// Database-first configuration manager
|
||||||
const path = require("path");
|
|
||||||
|
|
||||||
// Dynamic config that combines file-based config with database
|
|
||||||
class ConfigManager {
|
class ConfigManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.fileConfig = this.loadFileConfig();
|
|
||||||
this.databaseService = null; // Will be injected
|
this.databaseService = null; // Will be injected
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Load static configuration from file
|
|
||||||
*/
|
|
||||||
loadFileConfig() {
|
|
||||||
const configPath = path.join(__dirname, "..", "..", "config.json");
|
|
||||||
|
|
||||||
if (!fs.existsSync(configPath)) {
|
|
||||||
console.warn("config.json not found, using environment variables only");
|
|
||||||
return {
|
|
||||||
discord: {
|
|
||||||
token: process.env.DISCORD_TOKEN,
|
|
||||||
adminUserId: process.env.ADMIN_USER_ID,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
|
||||||
|
|
||||||
// Validate required fields
|
|
||||||
if (!config.discord?.token && !process.env.DISCORD_TOKEN) {
|
|
||||||
throw new Error(
|
|
||||||
"Discord token is required in config.json or DISCORD_TOKEN environment variable"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return config;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Inject database service (to avoid circular dependency)
|
* Inject database service (to avoid circular dependency)
|
||||||
*/
|
*/
|
||||||
@@ -44,89 +12,68 @@ class ConfigManager {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get bot configuration (combines file and database)
|
* Get bot configuration from database (with environment variable fallbacks)
|
||||||
*/
|
*/
|
||||||
getBotConfig() {
|
getBotConfig() {
|
||||||
const fileConfig = this.fileConfig;
|
if (!this.databaseService) {
|
||||||
const dbConfig = this.databaseService
|
// Fallback to environment variables if database not available
|
||||||
? this.databaseService.getBotConfiguration()
|
return {
|
||||||
: {};
|
botName: "GHBot",
|
||||||
|
debug: false,
|
||||||
|
discord: {
|
||||||
|
token: process.env.DISCORD_TOKEN,
|
||||||
|
adminUserId: process.env.ADMIN_USER_ID,
|
||||||
|
activities: ["Playing sounds", "Serving facts"],
|
||||||
|
blacklistedUsers: [],
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const dbConfig = this.databaseService.getBotConfiguration();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
// Use file config as fallback, database as primary
|
botName: dbConfig.botName || "GHBot",
|
||||||
botName: dbConfig.botName || fileConfig.botName || "GHBot",
|
debug: dbConfig.debug || false,
|
||||||
debug:
|
|
||||||
dbConfig.debug !== undefined
|
|
||||||
? dbConfig.debug
|
|
||||||
: fileConfig.debug || false,
|
|
||||||
discord: {
|
discord: {
|
||||||
token: fileConfig.discord?.token || process.env.DISCORD_TOKEN,
|
token: dbConfig.token || process.env.DISCORD_TOKEN,
|
||||||
adminUserId:
|
adminUserId: dbConfig.adminUserId || process.env.ADMIN_USER_ID,
|
||||||
dbConfig.adminUserId ||
|
activities: dbConfig.activities || ["Playing sounds", "Serving facts"],
|
||||||
fileConfig.discord?.adminUserId ||
|
blacklistedUsers: dbConfig.blacklistedUsers || [],
|
||||||
process.env.ADMIN_USER_ID,
|
|
||||||
activities: dbConfig.activities ||
|
|
||||||
fileConfig.discord?.activities || ["Playing sounds", "Serving facts"],
|
|
||||||
blacklistedUsers:
|
|
||||||
dbConfig.blacklistedUsers ||
|
|
||||||
fileConfig.discord?.blacklistedUsers ||
|
|
||||||
[],
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get guild configuration (from database primarily, file as fallback)
|
* Get guild configuration (database only)
|
||||||
*/
|
*/
|
||||||
getGuildConfig(guildId) {
|
getGuildConfig(guildId) {
|
||||||
if (this.databaseService) {
|
if (!this.databaseService) {
|
||||||
const dbConfig = this.databaseService.getGuildConfig(guildId);
|
// Return minimal default config if database not available
|
||||||
if (dbConfig) {
|
return {
|
||||||
return dbConfig;
|
id: guildId,
|
||||||
}
|
name: "Unknown Guild",
|
||||||
|
internalName: "Unknown Guild",
|
||||||
|
prefix: "!",
|
||||||
|
enableSfx: true,
|
||||||
|
sfxVolume: 0.5,
|
||||||
|
enableFunFacts: true,
|
||||||
|
enableHamFacts: true,
|
||||||
|
allowedRolesForRequest: [],
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to file config for backward compatibility
|
return this.databaseService.getGuildConfig(guildId);
|
||||||
if (this.fileConfig.discord?.guilds) {
|
|
||||||
const guilds = Array.isArray(this.fileConfig.discord.guilds)
|
|
||||||
? this.fileConfig.discord.guilds
|
|
||||||
: Object.values(this.fileConfig.discord.guilds);
|
|
||||||
|
|
||||||
return guilds.find((g) => g.id === guildId);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return default config for new guilds
|
|
||||||
return {
|
|
||||||
id: guildId,
|
|
||||||
name: "Unknown Guild",
|
|
||||||
internalName: "Unknown Guild",
|
|
||||||
prefix: "!",
|
|
||||||
enableSfx: true,
|
|
||||||
sfxVolume: 0.5,
|
|
||||||
enableFunFacts: true,
|
|
||||||
enableHamFacts: true,
|
|
||||||
allowedRolesForRequest: null,
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all guild configurations
|
* Get all guild configurations (database only)
|
||||||
*/
|
*/
|
||||||
getAllGuildConfigs() {
|
getAllGuildConfigs() {
|
||||||
if (this.databaseService) {
|
if (!this.databaseService) {
|
||||||
return this.databaseService.getAllGuildConfigs();
|
return [];
|
||||||
}
|
}
|
||||||
|
|
||||||
// Fallback to file config
|
return this.databaseService.getAllGuildConfigs();
|
||||||
if (this.fileConfig.discord?.guilds) {
|
|
||||||
const guilds = Array.isArray(this.fileConfig.discord.guilds)
|
|
||||||
? this.fileConfig.discord.guilds
|
|
||||||
: Object.values(this.fileConfig.discord.guilds);
|
|
||||||
|
|
||||||
return guilds;
|
|
||||||
}
|
|
||||||
|
|
||||||
return [];
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -67,6 +67,7 @@ class DatabaseService {
|
|||||||
INSERT OR IGNORE INTO bot_config (key, value) VALUES
|
INSERT OR IGNORE INTO bot_config (key, value) VALUES
|
||||||
('bot_name', 'GHBot'),
|
('bot_name', 'GHBot'),
|
||||||
('debug', 'false'),
|
('debug', 'false'),
|
||||||
|
('token', ''),
|
||||||
('admin_user_id', ''),
|
('admin_user_id', ''),
|
||||||
('activities', '["Chardee MacDennis", "The Nightman Cometh", "Charlie Work"]'),
|
('activities', '["Chardee MacDennis", "The Nightman Cometh", "Charlie Work"]'),
|
||||||
('blacklisted_users', '[]')
|
('blacklisted_users', '[]')
|
||||||
@@ -106,17 +107,17 @@ class DatabaseService {
|
|||||||
const fs = require("fs");
|
const fs = require("fs");
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
|
||||||
const configPath = path.join(__dirname, "..", "..", "config.json");
|
const seedPath = path.join(__dirname, "..", "..", "seed.json");
|
||||||
|
|
||||||
if (!fs.existsSync(configPath)) {
|
if (!fs.existsSync(seedPath)) {
|
||||||
console.log("No config.json file found, skipping seed");
|
console.log("No seed.json file found, skipping seed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const config = JSON.parse(fs.readFileSync(configPath, "utf-8"));
|
const config = JSON.parse(fs.readFileSync(seedPath, "utf-8"));
|
||||||
|
|
||||||
if (!config.discord?.guilds) {
|
if (!config.discord?.guilds) {
|
||||||
console.log("No guilds found in config.json, skipping seed");
|
console.log("No guilds found in seed.json, skipping seed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -180,7 +181,7 @@ class DatabaseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
console.log(
|
console.log(
|
||||||
`✅ Successfully seeded database with ${seededCount} guild(s) from config.json`
|
`✅ Successfully seeded database with ${seededCount} guild(s) from seed.json`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update bot configuration in database from file config
|
// Update bot configuration in database from file config
|
||||||
@@ -190,6 +191,9 @@ class DatabaseService {
|
|||||||
if (config.debug !== undefined) {
|
if (config.debug !== undefined) {
|
||||||
this.setBotConfig("debug", config.debug.toString());
|
this.setBotConfig("debug", config.debug.toString());
|
||||||
}
|
}
|
||||||
|
if (config.discord?.token) {
|
||||||
|
this.setBotConfig("token", config.discord.token);
|
||||||
|
}
|
||||||
if (config.discord?.adminUserId) {
|
if (config.discord?.adminUserId) {
|
||||||
this.setBotConfig("admin_user_id", config.discord.adminUserId);
|
this.setBotConfig("admin_user_id", config.discord.adminUserId);
|
||||||
}
|
}
|
||||||
@@ -212,7 +216,7 @@ class DatabaseService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log("✅ Bot configuration updated from config.json");
|
console.log("✅ Bot configuration updated from seed.json");
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error seeding database from config file:", error);
|
console.error("Error seeding database from config file:", error);
|
||||||
}
|
}
|
||||||
@@ -510,6 +514,7 @@ class DatabaseService {
|
|||||||
getBotConfiguration() {
|
getBotConfiguration() {
|
||||||
const botName = this.getBotConfig("bot_name") || "GHBot";
|
const botName = this.getBotConfig("bot_name") || "GHBot";
|
||||||
const debug = this.getBotConfig("debug") === "true";
|
const debug = this.getBotConfig("debug") === "true";
|
||||||
|
const token = this.getBotConfig("token") || "";
|
||||||
const adminUserId = this.getBotConfig("admin_user_id") || "";
|
const adminUserId = this.getBotConfig("admin_user_id") || "";
|
||||||
const activities = JSON.parse(this.getBotConfig("activities") || "[]");
|
const activities = JSON.parse(this.getBotConfig("activities") || "[]");
|
||||||
const blacklistedUsers = JSON.parse(
|
const blacklistedUsers = JSON.parse(
|
||||||
@@ -519,6 +524,7 @@ class DatabaseService {
|
|||||||
return {
|
return {
|
||||||
botName,
|
botName,
|
||||||
debug,
|
debug,
|
||||||
|
token,
|
||||||
adminUserId,
|
adminUserId,
|
||||||
activities,
|
activities,
|
||||||
blacklistedUsers,
|
blacklistedUsers,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
const schedule = require('node-schedule');
|
const schedule = require("node-schedule");
|
||||||
|
|
||||||
class SchedulerService {
|
class SchedulerService {
|
||||||
constructor() {
|
constructor() {
|
||||||
@@ -11,7 +11,7 @@ class SchedulerService {
|
|||||||
* @param {ConfigManager} configManager
|
* @param {ConfigManager} configManager
|
||||||
*/
|
*/
|
||||||
async initialize(client, configManager) {
|
async initialize(client, configManager) {
|
||||||
console.log('Initializing scheduled events...');
|
console.log("Initializing scheduled events...");
|
||||||
|
|
||||||
const guildConfigs = configManager.getAllGuildConfigs();
|
const guildConfigs = configManager.getAllGuildConfigs();
|
||||||
|
|
||||||
@@ -27,17 +27,22 @@ class SchedulerService {
|
|||||||
const databaseService = configManager.databaseService;
|
const databaseService = configManager.databaseService;
|
||||||
if (!databaseService) continue;
|
if (!databaseService) continue;
|
||||||
|
|
||||||
const scheduledEvents = databaseService.getScheduledEvents(guildConfig.id);
|
const scheduledEvents = databaseService.getScheduledEvents(
|
||||||
|
guildConfig.id
|
||||||
|
);
|
||||||
|
|
||||||
if (!scheduledEvents || scheduledEvents.length === 0) {
|
if (!scheduledEvents || scheduledEvents.length === 0) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const event of scheduledEvents) {
|
for (const event of scheduledEvents) {
|
||||||
await this.scheduleEvent(guild, event, guildConfig);
|
await this.scheduleEvent(guild, event);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error setting up scheduled events for guild ${guildConfig.id}:`, error);
|
console.error(
|
||||||
|
`Error setting up scheduled events for guild ${guildConfig.id}:`,
|
||||||
|
error
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -46,26 +51,29 @@ class SchedulerService {
|
|||||||
* Schedule a single event
|
* Schedule a single event
|
||||||
* @param {Guild} guild
|
* @param {Guild} guild
|
||||||
* @param {Object} event
|
* @param {Object} event
|
||||||
* @param {Object} guildConfig
|
|
||||||
*/
|
*/
|
||||||
async scheduleEvent(guild, event, guildConfig) {
|
async scheduleEvent(guild, event) {
|
||||||
try {
|
try {
|
||||||
// Validate channel
|
// Validate channel
|
||||||
let channel = null;
|
let channel = null;
|
||||||
if (event.channelId) {
|
if (event.channel_id) {
|
||||||
channel = await guild.channels.fetch(event.channelId);
|
channel = await guild.channels.fetch(event.channel_id);
|
||||||
if (!channel) {
|
if (!channel) {
|
||||||
console.error(`Invalid channel ${event.channelId} for event ${event.id} in guild ${guild.name}`);
|
console.error(
|
||||||
|
`Invalid channel ${event.channel_id} for event ${event.id} in guild ${guild.name}`
|
||||||
|
);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate role
|
// Validate role
|
||||||
let pingRole = null;
|
let pingRole = null;
|
||||||
if (event.pingRoleId) {
|
if (event.ping_role_id) {
|
||||||
pingRole = await guild.roles.fetch(event.pingRoleId);
|
pingRole = await guild.roles.fetch(event.ping_role_id);
|
||||||
if (!pingRole) {
|
if (!pingRole) {
|
||||||
console.warn(`Invalid role ${event.pingRoleId} for event ${event.id} in guild ${guild.name}`);
|
console.warn(
|
||||||
|
`Invalid role ${event.ping_role_id} for event ${event.id} in guild ${guild.name}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -81,9 +89,15 @@ class SchedulerService {
|
|||||||
const jobKey = `${guild.id}-${event.id}`;
|
const jobKey = `${guild.id}-${event.id}`;
|
||||||
this.jobs.set(jobKey, job);
|
this.jobs.set(jobKey, job);
|
||||||
|
|
||||||
console.log(`Event ${event.id} scheduled. Next invocation: ${job.nextInvocation()}`);
|
console.log(
|
||||||
|
`Event ${
|
||||||
|
event.id
|
||||||
|
} scheduled. Next invocation: ${job.nextInvocation()}`
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
console.error(`Failed to schedule event ${event.id} - invalid cron expression: ${event.schedule}`);
|
console.error(
|
||||||
|
`Failed to schedule event ${event.id} with schedule: ${event.schedule}`
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Error scheduling event ${event.id}:`, error);
|
console.error(`Error scheduling event ${event.id}:`, error);
|
||||||
@@ -112,7 +126,7 @@ class SchedulerService {
|
|||||||
|
|
||||||
// Send the message
|
// Send the message
|
||||||
if (content.length > 0 && channel) {
|
if (content.length > 0 && channel) {
|
||||||
await channel.send(content.join(' '));
|
await channel.send(content.join(" "));
|
||||||
console.log(`Executed scheduled event ${event.id}`);
|
console.log(`Executed scheduled event ${event.id}`);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -158,7 +172,7 @@ class SchedulerService {
|
|||||||
job.cancel();
|
job.cancel();
|
||||||
}
|
}
|
||||||
this.jobs.clear();
|
this.jobs.clear();
|
||||||
console.log('Cancelled all scheduled events');
|
console.log("Cancelled all scheduled events");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +1,47 @@
|
|||||||
const fs = require('fs');
|
const fs = require("fs");
|
||||||
const path = require('path');
|
const path = require("path");
|
||||||
const { MessageFlags } = require('discord.js');
|
const { MessageFlags } = require("discord.js");
|
||||||
const voiceService = require('./voiceService');
|
const voiceService = require("./voiceService");
|
||||||
|
|
||||||
class SFXManager {
|
class SfxManager {
|
||||||
constructor() {
|
constructor() {
|
||||||
this.sfxPath = path.join(__dirname, '..', '..', 'sfx');
|
this.sfxPath = path.join(__dirname, "..", "..", "sfx");
|
||||||
this.sfxList = [];
|
this.sfxList = [];
|
||||||
this.cachedNames = [];
|
this.cachedNames = [];
|
||||||
this.searchCache = new Map(); // Cache for autocomplete searches
|
this.searchCache = new Map(); // Cache for autocomplete searches
|
||||||
|
|
||||||
// Load SFX list initially
|
// Load SFX list initially
|
||||||
this.loadSFXList();
|
this.loadSfxList();
|
||||||
|
|
||||||
// Watch for changes
|
// Watch for changes
|
||||||
this.watchSFXDirectory();
|
this.watchSfxDirectory();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Load the list of available SFX files
|
* Load the list of available SFX files
|
||||||
*/
|
*/
|
||||||
loadSFXList() {
|
loadSfxList() {
|
||||||
try {
|
try {
|
||||||
if (!fs.existsSync(this.sfxPath)) {
|
if (!fs.existsSync(this.sfxPath)) {
|
||||||
console.log('SFX directory not found, creating...');
|
console.log("SFX directory not found, creating...");
|
||||||
fs.mkdirSync(this.sfxPath, { recursive: true });
|
fs.mkdirSync(this.sfxPath, { recursive: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
const files = fs.readdirSync(this.sfxPath);
|
const files = fs.readdirSync(this.sfxPath);
|
||||||
this.sfxList = files
|
this.sfxList = files
|
||||||
.filter(file => file.endsWith('.mp3') || file.endsWith('.wav'))
|
.filter((file) => file.endsWith(".mp3") || file.endsWith(".wav"))
|
||||||
.map(file => {
|
.map((file) => {
|
||||||
const ext = path.extname(file);
|
const ext = path.extname(file);
|
||||||
return {
|
return {
|
||||||
name: file.replace(ext, ''),
|
name: file.replace(ext, ""),
|
||||||
filename: file,
|
filename: file,
|
||||||
path: path.join(this.sfxPath, file)
|
path: path.join(this.sfxPath, file),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Cache sorted names for autocomplete
|
// Cache sorted names for autocomplete
|
||||||
this.cachedNames = this.sfxList
|
this.cachedNames = this.sfxList
|
||||||
.map(sfx => sfx.name)
|
.map((sfx) => sfx.name)
|
||||||
.sort((a, b) => a.localeCompare(b));
|
.sort((a, b) => a.localeCompare(b));
|
||||||
|
|
||||||
// Clear search cache when SFX list changes
|
// Clear search cache when SFX list changes
|
||||||
@@ -49,18 +49,18 @@ class SFXManager {
|
|||||||
|
|
||||||
console.log(`Loaded ${this.sfxList.length} sound effects`);
|
console.log(`Loaded ${this.sfxList.length} sound effects`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error loading SFX list:', error);
|
console.error("Error loading SFX list:", error);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Watch the SFX directory for changes
|
* Watch the SFX directory for changes
|
||||||
*/
|
*/
|
||||||
watchSFXDirectory() {
|
watchSfxDirectory() {
|
||||||
fs.watch(this.sfxPath, (eventType, filename) => {
|
fs.watch(this.sfxPath, (eventType, filename) => {
|
||||||
if (eventType === 'rename') {
|
if (eventType === "rename") {
|
||||||
console.log('SFX directory changed, reloading...');
|
console.log("SFX directory changed, reloading...");
|
||||||
this.loadSFXList();
|
this.loadSfxList();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -69,7 +69,7 @@ class SFXManager {
|
|||||||
* Get all available SFX
|
* Get all available SFX
|
||||||
* @returns {Array} List of SFX objects
|
* @returns {Array} List of SFX objects
|
||||||
*/
|
*/
|
||||||
getAllSFX() {
|
getAllSfx() {
|
||||||
return this.sfxList;
|
return this.sfxList;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ class SFXManager {
|
|||||||
* Get SFX names for autocomplete (cached and sorted)
|
* Get SFX names for autocomplete (cached and sorted)
|
||||||
* @returns {Array} List of SFX names
|
* @returns {Array} List of SFX names
|
||||||
*/
|
*/
|
||||||
getSFXNames() {
|
getSfxNames() {
|
||||||
return this.cachedNames;
|
return this.cachedNames;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,8 +86,10 @@ class SFXManager {
|
|||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @returns {Object|undefined} SFX object or undefined
|
* @returns {Object|undefined} SFX object or undefined
|
||||||
*/
|
*/
|
||||||
findSFX(name) {
|
findSfx(name) {
|
||||||
return this.sfxList.find(sfx => sfx.name.toLowerCase() === name.toLowerCase());
|
return this.sfxList.find(
|
||||||
|
(sfx) => sfx.name.toLowerCase() === name.toLowerCase()
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -95,8 +97,8 @@ class SFXManager {
|
|||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
*/
|
*/
|
||||||
hasSFX(name) {
|
hasSfx(name) {
|
||||||
return this.findSFX(name) !== undefined;
|
return this.findSfx(name) !== undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -104,8 +106,8 @@ class SFXManager {
|
|||||||
* @param {string} name
|
* @param {string} name
|
||||||
* @returns {string|null}
|
* @returns {string|null}
|
||||||
*/
|
*/
|
||||||
getSFXPath(name) {
|
getSfxPath(name) {
|
||||||
const sfx = this.findSFX(name);
|
const sfx = this.findSfx(name);
|
||||||
return sfx ? sfx.path : null;
|
return sfx ? sfx.path : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,7 +116,7 @@ class SFXManager {
|
|||||||
* @param {string} query
|
* @param {string} query
|
||||||
* @returns {Array} Matching SFX names
|
* @returns {Array} Matching SFX names
|
||||||
*/
|
*/
|
||||||
searchSFX(query) {
|
searchSfx(query) {
|
||||||
const lowerQuery = query.toLowerCase();
|
const lowerQuery = query.toLowerCase();
|
||||||
|
|
||||||
// Check cache first
|
// Check cache first
|
||||||
@@ -124,7 +126,7 @@ class SFXManager {
|
|||||||
|
|
||||||
// Perform search on cached names (already sorted)
|
// Perform search on cached names (already sorted)
|
||||||
const results = this.cachedNames
|
const results = this.cachedNames
|
||||||
.filter(name => name.toLowerCase().includes(lowerQuery))
|
.filter((name) => name.toLowerCase().includes(lowerQuery))
|
||||||
.slice(0, 25); // Discord autocomplete limit
|
.slice(0, 25); // Discord autocomplete limit
|
||||||
|
|
||||||
// Cache the result for future use
|
// Cache the result for future use
|
||||||
@@ -141,18 +143,25 @@ class SFXManager {
|
|||||||
* @param {string} commandType - Type of command ('slash' or 'soundboard')
|
* @param {string} commandType - Type of command ('slash' or 'soundboard')
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async playSFXInteraction(interaction, sfxName, guildConfig, commandType = 'slash') {
|
async playSfxInteraction(
|
||||||
|
interaction,
|
||||||
|
sfxName,
|
||||||
|
guildConfig,
|
||||||
|
commandType = "slash"
|
||||||
|
) {
|
||||||
// Log the request
|
// Log the request
|
||||||
const logPrefix = commandType === 'soundboard' ? 'Soundboard' : '/sfx';
|
const logPrefix = commandType === "soundboard" ? "Soundboard" : "/sfx";
|
||||||
console.log(
|
console.log(
|
||||||
`${logPrefix} '${sfxName}' requested in ${guildConfig.internalName || interaction.guild.name}#${interaction.channel.name} from @${interaction.user.username}`
|
`${logPrefix} '${sfxName}' requested in ${
|
||||||
|
guildConfig.internalName || interaction.guild.name
|
||||||
|
}#${interaction.channel.name} from @${interaction.user.username}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if SFX exists
|
// Check if SFX exists
|
||||||
if (!this.hasSFX(sfxName)) {
|
if (!this.hasSfx(sfxName)) {
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content: `❌ This sound effect does not exist!`,
|
content: `❌ This sound effect does not exist!`,
|
||||||
flags: [MessageFlags.Ephemeral]
|
flags: [MessageFlags.Ephemeral],
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -161,14 +170,14 @@ class SFXManager {
|
|||||||
// Immediately reply with playing status
|
// Immediately reply with playing status
|
||||||
await interaction.reply({
|
await interaction.reply({
|
||||||
content: `🔊 Playing: **${sfxName}**`,
|
content: `🔊 Playing: **${sfxName}**`,
|
||||||
flags: [MessageFlags.Ephemeral]
|
flags: [MessageFlags.Ephemeral],
|
||||||
});
|
});
|
||||||
|
|
||||||
// Join the voice channel
|
// Join the voice channel
|
||||||
await voiceService.join(interaction.member.voice.channel);
|
await voiceService.join(interaction.member.voice.channel);
|
||||||
|
|
||||||
// Get the SFX file path and play
|
// Get the SFX file path and play
|
||||||
const sfxPath = this.getSFXPath(sfxName);
|
const sfxPath = this.getSfxPath(sfxName);
|
||||||
await voiceService.play(interaction.guild.id, sfxPath, {
|
await voiceService.play(interaction.guild.id, sfxPath, {
|
||||||
volume: guildConfig.sfxVolume || 0.5,
|
volume: guildConfig.sfxVolume || 0.5,
|
||||||
});
|
});
|
||||||
@@ -176,10 +185,13 @@ class SFXManager {
|
|||||||
// Update the interaction to show completion
|
// Update the interaction to show completion
|
||||||
try {
|
try {
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
content: `✅ Finished playing: **${sfxName}**`
|
content: `✅ Finished playing: **${sfxName}**`,
|
||||||
});
|
});
|
||||||
} catch (editError) {
|
} catch (editError) {
|
||||||
console.error('Error updating interaction with completion message:', editError);
|
console.error(
|
||||||
|
"Error updating interaction with completion message:",
|
||||||
|
editError
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Leave the voice channel after playing
|
// Leave the voice channel after playing
|
||||||
@@ -187,18 +199,26 @@ class SFXManager {
|
|||||||
voiceService.leave(interaction.guild.id);
|
voiceService.leave(interaction.guild.id);
|
||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
console.log(`✅ Successfully played ${logPrefix.toLowerCase()} '${sfxName}'`);
|
console.log(
|
||||||
|
`✅ Successfully played ${logPrefix.toLowerCase()} '${sfxName}'`
|
||||||
|
);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Error playing ${logPrefix.toLowerCase()} '${sfxName}':`, error);
|
console.error(
|
||||||
|
`❌ Error playing ${logPrefix.toLowerCase()} '${sfxName}':`,
|
||||||
|
error
|
||||||
|
);
|
||||||
|
|
||||||
// Update the reply with error message
|
// Update the reply with error message
|
||||||
try {
|
try {
|
||||||
await interaction.editReply({
|
await interaction.editReply({
|
||||||
content: "❌ Couldn't play that sound effect. Make sure I have permission to join your voice channel!"
|
content:
|
||||||
|
"❌ Couldn't play that sound effect. Make sure I have permission to join your voice channel!",
|
||||||
});
|
});
|
||||||
} catch (editError) {
|
} catch (editError) {
|
||||||
console.error('Error updating interaction with error message:', editError);
|
console.error(
|
||||||
|
"Error updating interaction with error message:",
|
||||||
|
editError
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -210,33 +230,35 @@ class SFXManager {
|
|||||||
* @param {Object} guildConfig - Guild configuration
|
* @param {Object} guildConfig - Guild configuration
|
||||||
* @returns {Promise<void>}
|
* @returns {Promise<void>}
|
||||||
*/
|
*/
|
||||||
async playSFXMessage(message, sfxName, guildConfig) {
|
async playSfxMessage(message, sfxName, guildConfig) {
|
||||||
// Log the request
|
// Log the request
|
||||||
console.log(
|
console.log(
|
||||||
`SFX '${sfxName}' requested in ${guildConfig.internalName || message.guild.name}#${message.channel.name} from @${message.author.username}`
|
`SFX '${sfxName}' requested in ${
|
||||||
|
guildConfig.internalName || message.guild.name
|
||||||
|
}#${message.channel.name} from @${message.author.username}`
|
||||||
);
|
);
|
||||||
|
|
||||||
// Check if SFX exists
|
// Check if SFX exists
|
||||||
if (!this.hasSFX(sfxName)) {
|
if (!this.hasSfx(sfxName)) {
|
||||||
await message.reply('❌ This sound effect does not exist!');
|
await message.reply("❌ This sound effect does not exist!");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// React with speaker icon to show playing status
|
// React with speaker icon to show playing status
|
||||||
await message.react('🔊');
|
await message.react("🔊");
|
||||||
|
|
||||||
// Join the voice channel
|
// Join the voice channel
|
||||||
await voiceService.join(message.member.voice.channel);
|
await voiceService.join(message.member.voice.channel);
|
||||||
|
|
||||||
// Get the SFX file path and play
|
// Get the SFX file path and play
|
||||||
const sfxPath = this.getSFXPath(sfxName);
|
const sfxPath = this.getSfxPath(sfxName);
|
||||||
await voiceService.play(message.guild.id, sfxPath, {
|
await voiceService.play(message.guild.id, sfxPath, {
|
||||||
volume: guildConfig.sfxVolume || 0.5,
|
volume: guildConfig.sfxVolume || 0.5,
|
||||||
});
|
});
|
||||||
|
|
||||||
// Add completion reaction (keep both speaker and checkmark)
|
// Add completion reaction (keep both speaker and checkmark)
|
||||||
await message.react('✅');
|
await message.react("✅");
|
||||||
|
|
||||||
// Leave the voice channel after playing
|
// Leave the voice channel after playing
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
@@ -244,19 +266,20 @@ class SFXManager {
|
|||||||
}, 500);
|
}, 500);
|
||||||
|
|
||||||
console.log(`✅ Successfully played SFX '${sfxName}'`);
|
console.log(`✅ Successfully played SFX '${sfxName}'`);
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`❌ Error playing SFX '${sfxName}':`, error);
|
console.error(`❌ Error playing SFX '${sfxName}':`, error);
|
||||||
|
|
||||||
// Add error reaction
|
// Add error reaction
|
||||||
try {
|
try {
|
||||||
await message.react('❌');
|
await message.react("❌");
|
||||||
} catch (reactionError) {
|
} catch (reactionError) {
|
||||||
// If reactions fail, fall back to reply
|
// If reactions fail, fall back to reply
|
||||||
await message.reply("❌ Couldn't play that sound effect. Make sure I have permission to join your voice channel!");
|
await message.reply(
|
||||||
|
"❌ Couldn't play that sound effect. Make sure I have permission to join your voice channel!"
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
module.exports = new SFXManager();
|
module.exports = new SfxManager();
|
||||||
|
|||||||
@@ -6,13 +6,12 @@ const {
|
|||||||
AudioPlayerStatus,
|
AudioPlayerStatus,
|
||||||
entersState,
|
entersState,
|
||||||
getVoiceConnection,
|
getVoiceConnection,
|
||||||
generateDependencyReport
|
} = require("@discordjs/voice");
|
||||||
} = require('@discordjs/voice');
|
const { ChannelType } = require("discord.js");
|
||||||
const { ChannelType } = require('discord.js');
|
|
||||||
|
|
||||||
// Try to use ffmpeg-static as fallback if system ffmpeg is not available
|
// Try to use ffmpeg-static as fallback if system ffmpeg is not available
|
||||||
try {
|
try {
|
||||||
const ffmpegPath = require('ffmpeg-static');
|
const ffmpegPath = require("ffmpeg-static");
|
||||||
if (ffmpegPath && !process.env.FFMPEG_PATH) {
|
if (ffmpegPath && !process.env.FFMPEG_PATH) {
|
||||||
process.env.FFMPEG_PATH = ffmpegPath;
|
process.env.FFMPEG_PATH = ffmpegPath;
|
||||||
}
|
}
|
||||||
@@ -33,7 +32,7 @@ class VoiceService {
|
|||||||
*/
|
*/
|
||||||
async join(channel) {
|
async join(channel) {
|
||||||
if (!channel || channel.type !== ChannelType.GuildVoice) {
|
if (!channel || channel.type !== ChannelType.GuildVoice) {
|
||||||
throw new Error('Invalid voice channel');
|
throw new Error("Invalid voice channel");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Check if already connected
|
// Check if already connected
|
||||||
@@ -100,7 +99,7 @@ class VoiceService {
|
|||||||
async play(guildId, filePath, options = {}) {
|
async play(guildId, filePath, options = {}) {
|
||||||
const connection = this.connections.get(guildId);
|
const connection = this.connections.get(guildId);
|
||||||
if (!connection) {
|
if (!connection) {
|
||||||
throw new Error('Not connected to voice channel');
|
throw new Error("Not connected to voice channel");
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create or get player for this guild
|
// Create or get player for this guild
|
||||||
@@ -131,8 +130,8 @@ class VoiceService {
|
|||||||
resolve();
|
resolve();
|
||||||
});
|
});
|
||||||
|
|
||||||
player.once('error', (error) => {
|
player.once("error", (error) => {
|
||||||
console.error('Player error:', error);
|
console.error("Player error:", error);
|
||||||
reject(error);
|
reject(error);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user