- improved lyric command
- improved eval
- add reloadcmd command
- update commands to use collection instead of array
This commit is contained in:
frostice482 2025-01-26 12:51:43 +07:00
parent 12cc645120
commit 70a872de45
6 changed files with 291 additions and 117 deletions

View File

@ -12,5 +12,6 @@
"@structures/*": ["./src/structures/*"]
}
},
"include": ["src"],
"exclude": ["node_modules", "**/node_modules/*"]
}

View File

@ -1,5 +1,5 @@
const { EmbedBuilder, ApplicationCommandOptionType } = require("discord.js");
const { MESSAGES, EMBED_COLORS } = require("@root/config");
const { EMBED_COLORS } = require("@root/config");
/**
* @type {import("@structures/Command")}
@ -39,86 +39,93 @@ module.exports = {
};
async function getLyric({ client, guild, member }, query) {
const player = client.musicManager.getPlayer(guild.id);
if (!player) {
return "🚫 There's no active music player in this server.";
}
/** @type {import('../../handlers/manager')} */
const manager = client.musicManager
const player = manager.getPlayer(guild.id)
let node = player?.node
if (!node) [node] = manager.nodeManager.nodes.values()
let track;
let lyrics;
if (!query) {
// query not specified - use currently playing music
track = player?.queue.current;
if (!track) {
return "🚫 No music is currently playing!";
}
} else {
// query specified -- search
const result = await node.search({ query }, member.user);
if (!result || result.loadType === "error" || result.loadType === "empty") {
return "Failed to find any tracks for the query.";
}
track = result.tracks[0];
}
// lavalyrics
try {
const lyrics = await node.request(`/lyrics?track=${encodeURIComponent(track.encoded)}&skipTrackSource=${true}`)
if (lyrics && 'text' in lyrics) return createLyricsEmbed(lyrics, member, track)
else console.log(`Failed search LavaLyrics track ${track.info.title}`, lyrics)
} catch (e) {
console.error(`Error search LavaLyrics track ${track.info.title}`, e)
}
// javatimed
if (node.info.plugins.includes('java-timed-lyrics')) {
// java-timed (current)
if (!query) {
if (!player.queue.current) {
return "🚫 No music is currently playing!";
try {
const lyrics = await getCurrentLyricJavaTimed(node, guild.id)
if (lyrics && 'text' in lyrics) return createLyricsEmbed(lyrics, member, track)
else console.log(`Failed search JavaTimed (current) track ${track.info.title}`, lyrics)
} catch (e) {
console.error(`Error search JavaTimed (current) track ${track.info.title}`, e)
}
track = player.queue.current;
} else {
const result = await player.search({ query }, member.user);
if (!result || result.loadType === "error" || result.loadType === "empty") {
return "Failed to find any tracks for the query.";
}
track = result.tracks[0];
}
const node = player.node;
const baseUrl = (node.options.port !== 80 && node.options.secure)
? `https://${node.options.host}:${node.options.port}`
: `http://${node.options.host}:${node.options.port}`;
// Try default lyrics endpoint first
// java-timed (search)
try {
const defaultLyricsUrl = `${baseUrl}/v4/sessions/${node.sessionId}/players/${guild.id}/track/lyrics?skipTrackSource=true`;
const defaultRes = await fetch(defaultLyricsUrl, {
headers: { Authorization: node.options.authorization }
});
if (defaultRes.ok) {
lyrics = await defaultRes.json();
if (lyrics) {
return createLyricsEmbed(lyrics, member, track);
const lyrics = await searchLyricJavaTimed(node, guild.id)
if (lyrics.lines) {
for (const line of lyrics.lines) {
line.timestamp = line.range.start
line.duration = line.range.end - line.range.start
}
}
} catch (err) {
client.logger.debug("Default lyrics endpoint failed:", err);
if (lyrics && 'text' in lyrics) return createLyricsEmbed(lyrics, member, track)
else console.log(`Failed search JavaTimed (search) track ${track.info.title}`, lyrics)
} catch (e) {
console.error(`Error search JavaTimed (search) track ${track.info.title}`, e)
}
// Fallback to genius search if default endpoint fails
const geniusUrl = `${baseUrl}/v4/lyrics/search?query=${encodeURIComponent(track.info.title + " " + track.info.author)}&source=genius`;
const geniusRes = await fetch(geniusUrl, {
headers: { Authorization: node.options.authorization }
});
if (!geniusRes.ok) {
if (geniusRes.status === 404) return "No lyrics found for this song.";
throw new Error(`Failed to fetch lyrics: ${geniusRes.status} ${geniusRes.statusText}`);
}
lyrics = await geniusRes.json();
return createLyricsEmbed(lyrics, member, track);
} catch (error) {
client.logger.error("Lyric Command Error:", error);
return "An error occurred while fetching the lyrics. Please try again later.";
}
return "No lyrics found"
}
function createLyricsEmbed(lyrics, member, track) {
if (!lyrics) {
return "No lyrics found for this song.";
}
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.BOT_EMBED)
.setTitle(`${track.info.author} - ${track.info.title}`)
.setThumbnail(track.info.artworkUrl)
.setFooter({ text: `Requested by: ${member.user.displayName} | Source: ${lyrics.source || 'Unknown'}` });
.setFooter({ text: `Requested by: ${member.user.displayName} | Source: ${lyrics.provider || lyrics.source || lyrics.sourceName || '-'}` });
const lines = !lyrics.lines ? lyrics.text ?? 'No lyrics found' : lyrics.lines
.filter(Boolean)
.map(line => line.line)
.join("\n");
const lines = lyrics.lines.map(line => line.line).filter(Boolean).join("\n");
const truncatedLyrics = lines.length > 4096 ? `${lines.slice(0, 4093)}...` : lines;
embed.setDescription(truncatedLyrics);
return { embeds: [embed] };
}
function getCurrentLyricJavaTimed(node, guildId) {
return node.request(`/sessions/${node.sessionId}/players/${guildId}/lyrics`)
}
function searchLyricJavaTimed(node, track, source = "genius") {
return node.request(`/lyrics/search?query=${encodeURIComponent(track.info.title + " " + track.info.author)}&source=${source}`)
}

View File

@ -1,9 +1,13 @@
const { EmbedBuilder, ApplicationCommandOptionType } = require("discord.js");
const { EmbedBuilder, ApplicationCommandOptionType, AttachmentBuilder } = require("discord.js");
const { EMBED_COLORS } = require("@root/config");
// This dummy token will be replaced by the actual token
const DUMMY_TOKEN = "MY_TOKEN_IS_SECRET";
const AsyncFunctionConstructor = (async() => {}).constructor
let prev = null
let store = Object.create(null)
/**
* @type {import("@structures/Command")}
*/
@ -13,71 +17,113 @@ module.exports = {
category: "OWNER",
botPermissions: ["EmbedLinks"],
command: {
enabled: true,
enabled: process.env.DEV === 'true',
usage: "<script>",
minArgsCount: 1,
},
slashCommand: {
enabled: false,
enabled: process.env.DEV === 'true',
options: [
{
name: "expression",
description: "content to evaluate",
description: "Code to evaluate",
type: ApplicationCommandOptionType.String,
required: true,
required: false,
},
{
name: "attachment",
description: "Code to evaluate",
type: ApplicationCommandOptionType.Attachment,
required: false,
},
{
name: "async",
description: "Use async - requires return to retreive value",
type: ApplicationCommandOptionType.Boolean,
required: false
}
],
},
async messageRun(message, args) {
const input = args.join(" ");
async messageRun(message, args, data) {
let content = message.content.slice(data.prefix.length + data.invoke.length).trim()
if (!input) return message.safeReply("Please provide code to eval");
let response;
try {
const output = eval(input);
response = buildSuccessResponse(output, message.client);
} catch (ex) {
response = buildErrorResponse(ex);
if (content.startsWith('```')) {
content = content.replace(/^```(js)?/, '')
if (content.endsWith('```')) content = content.slice(0, -3)
}
await message.safeReply(response);
else if (content.startsWith('``')) {
content = content.slice(2, -2)
}
const attachment = message.attachments.at(0)
await message.reply(await execute(false, attachment, content, { message, args, data }, message.client));
},
async interactionRun(interaction) {
const input = interaction.options.getString("expression");
async interactionRun(interaction, data) {
const attachment = interaction.options.getAttachment("attachment")
const code = interaction.options.getString("expression")
const useAsync = interaction.options.getBoolean("async") ?? false
let response;
try {
const output = eval(input);
response = buildSuccessResponse(output, interaction.client);
} catch (ex) {
response = buildErrorResponse(ex);
}
await interaction.followUp(response);
await interaction.followUp(await execute(useAsync, attachment, code, { interaction, data }, interaction.client));
},
};
const buildSuccessResponse = (output, client) => {
async function execute(useAsync, attachment, code, context, client) {
if (!code) {
if (!attachment) return "Specify code or attachment to run"
if (attachment.contentType !== 'text/javascript') return "Attachment type must be JavaScript"
const res = await fetch(attachment.url)
if (!res.ok && !code) return "Failed to retreive attachment"
code = await res.text()
}
Object.assign(context, {
client,
store: store,
_: prev
})
try {
const exec = useAsync
? new AsyncFunctionConstructor('ctx', `with (ctx) {${code}}; return "return not set"`)
: new Function('ctx', `with (ctx) return eval(${JSON.stringify(code)})`)
const output = await exec.call(client, context)
prev = output
return buildResponse(output, client);
} catch (ex) {
return buildResponse(ex, client, true);
}
}
const buildResponse = (output, client, isError) => {
// Token protection
output = require("util").inspect(output, { depth: 0 }).replaceAll(client.token, DUMMY_TOKEN);
output = require("util").inspect(output, {
maxArrayLength: 5,
maxStringLength: 500,
depth: 3
}).replaceAll(client.token, DUMMY_TOKEN);
const embed = new EmbedBuilder()
.setAuthor({ name: "📤 Output" })
.setDescription("```js\n" + (output.length > 4096 ? `${output.substr(0, 4000)}...` : output) + "\n```")
.setColor("Random")
.setTimestamp(Date.now());
// use file
if (output.length > 200 || (output.match(/\n/g)?.length ?? 0) > 10) {
const file = new AttachmentBuilder(Buffer.from(output), { name: 'output.js' })
return { embeds: [embed] };
};
const buildErrorResponse = (err) => {
const embed = new EmbedBuilder();
embed
.setAuthor({ name: "📤 Error" })
.setDescription("```js\n" + (err.length > 4096 ? `${err.substr(0, 4000)}...` : err) + "\n```")
.setColor(EMBED_COLORS.ERROR)
.setTimestamp(Date.now());
return { embeds: [embed] };
return {
content: isError ? 'Error!' : 'Output:',
files: [file]
}
}
// use embed
else {
const embed = new EmbedBuilder()
.setAuthor({ name: isError ? "📤 Error" : "📤 Output" })
.setDescription("```js\n" + output + "\n```")
.setColor(isError ? EMBED_COLORS.ERROR : EMBED_COLORS.SUCCESS)
.setTimestamp(Date.now());
return { embeds: [embed] }
}
};

View File

@ -0,0 +1,120 @@
const path = require('path')
const { ApplicationCommandOptionType, ApplicationCommandType } = require('discord.js')
/**
* @type {import("@structures/Command")}
*/
module.exports = {
name: "reloadcmd",
description: "Reloads commands",
category: "OWNER",
command: {
enabled: process.env.DEV === 'true',
usage: "<filename> [guild]",
minArgsCount: 1,
},
slashCommand: {
enabled: process.env.DEV === 'true',
options: [
{
name: "filename",
description: "File name to load. Reloads all if not specified",
type: ApplicationCommandOptionType.String,
required: true,
}, {
name: "guild",
description: "guild",
type: ApplicationCommandOptionType.String,
required: false,
}, {
name: "reregister",
description: "Reregisters command to manager",
type: ApplicationCommandOptionType.Boolean,
required: false,
},
],
},
async messageRun(message, args) {
await message.safeReply(await runReload(message.client, args[0], args[1]))
},
async interactionRun(interaction) {
await interaction.followUp(await runReload(
interaction.client,
interaction.options.getString("filename"),
interaction.options.getString("guild"),
interaction.options.getBoolean("reregister"),
))
}
};
async function runReload(client, filename, guildid, rereg = false) {
// guild id
let guild
if (guildid) {
guild = client.guilds.cache.get(guildid);
if (!guild) return "No matching guild"
}
// load
if (filename[0] === '/') {
const cmdname = filename.slice(1)
const cmd = client.commands.get(cmdname)
if (!cmd) return "Cannot find command with name " + cmd
if (!cmd.category) return `Command category ${cmd} is undefined`
filename = cmd.category.toLowerCase() + '/' + cmdname
}
if (!path.extname(filename)) filename += '.js'
const target = path.resolve('src', 'commands', path.normalize(filename))
const oldCmd = require.cache[target]?.exports
const oldSlashEnable = oldCmd?.slashCommand?.enabled
delete require.cache[target]
const newCmd = require(target)
const newSlashEnable = newCmd?.slashCommand?.enabled
if (rereg) {
// register / edit
const cmdManager = guild ? guild.commands : client.application.commands
const slashCmd = {
name: newCmd.name,
description: newCmd.description,
type: ApplicationCommandType.ChatInput,
options: newCmd.slashCommand?.options
}
// slash commands
if (oldSlashEnable) {
const oldSlash = client.application.commands.cache.find(v => v.name === oldCmd.name)
if (oldSlash) {
if (newSlashEnable) await cmdManager.edit(oldSlash, slashCmd)
else await cmdManager.delete(oldSlash)
}
}
else if (newSlashEnable) {
await cmdManager.create(slashCmd)
}
}
// remove old
if (oldCmd?.command?.enabled) {
client.commands.delete(oldCmd.name.toLowerCase())
for (const alias of oldCmd.command.aliases ?? []) client.commandAlias.delete(alias)
}
if (oldSlashEnable) {
client.slashCommands.delete(oldCmd.name);
}
// add new
if (newCmd?.command?.enabled) {
client.commands.set(newCmd.name.toLowerCase(), newCmd)
for (const alias of newCmd.command.aliases ?? []) client.commandAlias.set(alias, newCmd)
}
if (newSlashEnable) {
client.slashCommands.set(newCmd.name, newCmd);
}
return `Reloaded ${filename}`
}

View File

@ -310,7 +310,7 @@ function getMsgCategoryEmbeds(client, category, prefix) {
// For REMAINING Categories
const commands = client.commands.filter((cmd) => cmd.category === category);
if (commands.length === 0) {
if (commands.size === 0) {
const embed = new EmbedBuilder()
.setColor(EMBED_COLORS.BOT_EMBED)
.setThumbnail(CommandCategory[category]?.image)
@ -323,8 +323,9 @@ function getMsgCategoryEmbeds(client, category, prefix) {
const arrSplitted = [];
const arrEmbeds = [];
while (commands.length) {
let toAdd = commands.splice(0, commands.length > CMDS_PER_PAGE ? CMDS_PER_PAGE : commands.length);
const cmdArr = Array.from(commands.values())
while (cmdArr.length) {
let toAdd = cmdArr.splice(0, cmdArr.size > CMDS_PER_PAGE ? CMDS_PER_PAGE : cmdArr.size);
toAdd = toAdd.map((cmd) => `\`${prefix}${cmd.name}\`\n ${cmd.description}\n`);
arrSplitted.push(toAdd);
}

View File

@ -43,10 +43,10 @@ module.exports = class BotClient extends Client {
this.config = require("@root/config"); // load the config file
/**
* @type {import('@structures/Command')[]}
* @type {Collection<string, import('@structures/Command')>}
*/
this.commands = []; // store actual command
this.commandIndex = new Collection(); // store (alias, arrayIndex) pair
this.commands = new Collection(); // store actual command
this.commandAlias = new Map(); // store (alias, command) pair
/**
* @type {Collection<string, import('@structures/Command')>}
@ -135,8 +135,8 @@ module.exports = class BotClient extends Client {
* @returns {import('@structures/Command')|undefined}
*/
getCommand(invoke) {
const index = this.commandIndex.get(invoke.toLowerCase());
return index !== undefined ? this.commands[index] : undefined;
const invokeLower = invoke.toLowerCase()
return this.commands.get(invokeLower) ?? this.commandAlias.get(invokeLower);
}
/**
@ -151,18 +151,17 @@ module.exports = class BotClient extends Client {
}
// Prefix Command
if (cmd.command?.enabled) {
const index = this.commands.length;
if (this.commandIndex.has(cmd.name)) {
const name = cmd.name.toLowerCase()
if (this.commands.has(name)) {
throw new Error(`Command ${cmd.name} already registered`);
}
if (Array.isArray(cmd.command.aliases)) {
cmd.command.aliases.forEach((alias) => {
if (this.commandIndex.has(alias)) throw new Error(`Alias ${alias} already registered`);
this.commandIndex.set(alias.toLowerCase(), index);
if (this.commandAlias.has(alias)) throw new Error(`Alias ${alias} already registered`);
this.commandAlias.set(alias.toLowerCase(), cmd);
});
}
this.commandIndex.set(cmd.name.toLowerCase(), index);
this.commands.push(cmd);
this.commands.set(name, cmd);
} else {
this.logger.debug(`Skipping command ${cmd.name}. Disabled!`);
}
@ -195,7 +194,7 @@ module.exports = class BotClient extends Client {
}
}
this.logger.success(`Loaded ${this.commands.length} commands`);
this.logger.success(`Loaded ${this.commands.size} commands`);
this.logger.success(`Loaded ${this.slashCommands.size} slash commands`);
if (this.slashCommands.size > 100) throw new Error("A maximum of 100 slash commands can be enabled");
}