mirror of
https://github.com/MinazukiAmane/Tinasha-Bot.git
synced 2025-03-15 04:15:58 +08:00
commit
- improved lyric command - improved eval - add reloadcmd command - update commands to use collection instead of array
This commit is contained in:
parent
12cc645120
commit
70a872de45
@ -12,5 +12,6 @@
|
||||
"@structures/*": ["./src/structures/*"]
|
||||
}
|
||||
},
|
||||
"include": ["src"],
|
||||
"exclude": ["node_modules", "**/node_modules/*"]
|
||||
}
|
@ -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}`)
|
||||
}
|
||||
|
@ -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] }
|
||||
}
|
||||
};
|
||||
|
120
src/commands/owner/reloadcmd.js
Normal file
120
src/commands/owner/reloadcmd.js
Normal 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}`
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -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");
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user