mirror of
https://github.com/ReVanced/revanced-bots.git
synced 2026-01-11 13:56:15 +00:00
feat(bots/discord): add train commands
This commit is contained in:
76
bots/discord/src/commands/support/train/chat.ts
Normal file
76
bots/discord/src/commands/support/train/chat.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import Command from '$/classes/Command'
|
||||
import CommandError, { CommandErrorType } from '$/classes/CommandError'
|
||||
import { config } from '../../../context'
|
||||
import type { FetchMessageOptions, MessageResolvable } from 'discord.js'
|
||||
import type { ConfigMessageScanResponseLabelConfig } from 'config.schema'
|
||||
import { createSuccessEmbed } from '$/utils/discord/embeds'
|
||||
|
||||
const msRcConfig = config.messageScan?.humanCorrections?.allow
|
||||
|
||||
export default new Command({
|
||||
name: 'train',
|
||||
description: 'Train a specific message or text to a specific label',
|
||||
type: Command.Type.ChatGuild,
|
||||
requirements: {
|
||||
users: msRcConfig?.users,
|
||||
roles: msRcConfig?.members?.roles,
|
||||
permissions: msRcConfig?.members?.permissions,
|
||||
mode: 'any',
|
||||
memberRequirementsForUsers: 'fail',
|
||||
defaultCondition: 'fail',
|
||||
},
|
||||
options: {
|
||||
message: {
|
||||
description: 'The message to train (use `latest` to train the latest message)',
|
||||
type: Command.OptionType.String,
|
||||
required: true,
|
||||
},
|
||||
label: {
|
||||
description: 'The label to train the message as',
|
||||
type: Command.OptionType.String,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
allowMessageCommand: true,
|
||||
async execute(context, trigger, { label, message: ref }) {
|
||||
const { logger, config } = context
|
||||
const { messageScan: msConfig } = config
|
||||
|
||||
// If there's no config, we can't do anything
|
||||
if (!msConfig?.humanCorrections) throw new CommandError(CommandErrorType.Generic, 'Response correction is off.')
|
||||
const labels = msConfig.responses?.flatMap(r =>
|
||||
r.triggers.text!.filter((t): t is ConfigMessageScanResponseLabelConfig => 'label' in t).map(t => t.label),
|
||||
)
|
||||
|
||||
const channel = await trigger.guild!.channels.fetch(trigger.channelId)
|
||||
if (!channel?.isTextBased())
|
||||
throw new CommandError(
|
||||
CommandErrorType.InvalidArgument,
|
||||
'This command can only be used in or on text channels',
|
||||
)
|
||||
|
||||
if (!labels.includes(label))
|
||||
throw new CommandError(
|
||||
CommandErrorType.InvalidArgument,
|
||||
`The provided label is invalid.\nValid labels are:${labels.map(l => `\n- \`${l}\``).join('')}`,
|
||||
)
|
||||
|
||||
const refMsg = await channel.messages.fetch(
|
||||
(ref.startsWith('latest') ? { limit: 1 } : ref) as MessageResolvable | FetchMessageOptions,
|
||||
)
|
||||
if (!refMsg) throw new CommandError(CommandErrorType.InvalidArgument, 'The provided message does not exist.')
|
||||
|
||||
logger.debug(`User ${context.executor.id} is training message ${refMsg?.id} as ${label}`)
|
||||
|
||||
await context.api.client.trainMessage(refMsg.content, label)
|
||||
await trigger.reply({
|
||||
embeds: [
|
||||
createSuccessEmbed(
|
||||
'Message trained',
|
||||
`The provided message has been trained as \`${label}\`. Thank you for your contribution!`,
|
||||
),
|
||||
],
|
||||
ephemeral: true,
|
||||
})
|
||||
},
|
||||
})
|
||||
50
bots/discord/src/commands/support/train/context-menu.ts
Normal file
50
bots/discord/src/commands/support/train/context-menu.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
import Command from '$/classes/Command'
|
||||
import CommandError, { CommandErrorType } from '$/classes/CommandError'
|
||||
import { config } from '../../../context'
|
||||
import { type APIStringSelectComponent, ComponentType } from 'discord.js'
|
||||
import type { ConfigMessageScanResponseLabelConfig } from 'config.schema'
|
||||
|
||||
const msRcConfig = config.messageScan?.humanCorrections?.allow
|
||||
|
||||
export default new Command({
|
||||
name: 'Train Message',
|
||||
type: Command.Type.ContextMenuGuildMessage,
|
||||
requirements: {
|
||||
users: msRcConfig?.users,
|
||||
roles: msRcConfig?.members?.roles,
|
||||
permissions: msRcConfig?.members?.permissions,
|
||||
mode: 'any',
|
||||
memberRequirementsForUsers: 'fail',
|
||||
defaultCondition: 'fail',
|
||||
},
|
||||
async execute(context, trigger) {
|
||||
const { logger, config } = context
|
||||
const { messageScan: msConfig } = config
|
||||
|
||||
// If there's no config, we can't do anything
|
||||
if (!msConfig?.humanCorrections) throw new CommandError(CommandErrorType.Generic, 'Response correction is off.')
|
||||
|
||||
logger.debug(`User ${context.executor.id} is training message ${trigger.targetId}`)
|
||||
|
||||
const labels = msConfig.responses.flatMap(r =>
|
||||
r.triggers.text!.filter((t): t is ConfigMessageScanResponseLabelConfig => 'label' in t).map(t => t.label),
|
||||
)
|
||||
|
||||
await trigger.reply({
|
||||
content: 'Select a label to train this message as:',
|
||||
components: [
|
||||
{
|
||||
components: [
|
||||
{
|
||||
custom_id: `tr_${trigger.targetMessage.channelId}_${trigger.targetId}`,
|
||||
options: labels.map(label => ({ label, value: label })),
|
||||
type: ComponentType.StringSelect,
|
||||
} satisfies APIStringSelectComponent,
|
||||
],
|
||||
type: ComponentType.ActionRow,
|
||||
},
|
||||
],
|
||||
ephemeral: true,
|
||||
})
|
||||
},
|
||||
})
|
||||
@@ -18,10 +18,10 @@ withContext(on, 'interactionCreate', async (context, interaction) => {
|
||||
if (!interaction.isStringSelectMenu() && !interaction.isButton()) return
|
||||
if (!interaction.customId.startsWith('cr_')) return
|
||||
|
||||
const [, key, action] = interaction.customId.split('_') as ['cr', string, 'select' | 'cancel' | 'delete']
|
||||
if (!key || !action) return
|
||||
const [, msgId, action] = interaction.customId.split('_') as ['cr', string, 'select' | 'cancel' | 'delete']
|
||||
if (!msgId || !action) return
|
||||
|
||||
const response = await db.query.responses.findFirst({ where: eq(responses.replyId, key) })
|
||||
const response = await db.query.responses.findFirst({ where: eq(responses.replyId, msgId) })
|
||||
// If the message isn't saved in my DB (unrelated message)
|
||||
if (!response)
|
||||
return void (await interaction.reply({
|
||||
@@ -32,11 +32,11 @@ withContext(on, 'interactionCreate', async (context, interaction) => {
|
||||
try {
|
||||
// We're gonna pretend reactionChannel is a text-based channel, but it can be many more
|
||||
// But `messages` should always exist as a property
|
||||
const reactionGuild = await interaction.client.guilds.fetch(response.guildId)
|
||||
const reactionChannel = (await reactionGuild.channels.fetch(response.channelId)) as TextBasedChannel | null
|
||||
const reactionMessage = await reactionChannel?.messages.fetch(key)
|
||||
const guild = await interaction.client.guilds.fetch(response.guildId)
|
||||
const channel = (await guild.channels.fetch(response.channelId)) as TextBasedChannel | null
|
||||
const msg = await channel?.messages.fetch(msgId)
|
||||
|
||||
if (!reactionMessage) {
|
||||
if (!msg) {
|
||||
await interaction.deferUpdate()
|
||||
await interaction.message.edit({
|
||||
content: null,
|
||||
@@ -53,9 +53,9 @@ withContext(on, 'interactionCreate', async (context, interaction) => {
|
||||
}
|
||||
|
||||
const editMessage = (content: string, description?: string) =>
|
||||
editInteractionMessage(interaction, reactionMessage.url, content, description)
|
||||
editInteractionMessage(interaction, msg.url, content, description)
|
||||
const handleCorrection = (label: string) =>
|
||||
handleUserResponseCorrection(context, response, reactionMessage, label, interaction.user)
|
||||
handleUserResponseCorrection(context, response, msg, label, interaction.user)
|
||||
|
||||
if (response.correctedById)
|
||||
return await editMessage(
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
import { createErrorEmbed, createStackTraceEmbed, createSuccessEmbed } from '$utils/discord/embeds'
|
||||
import { on, withContext } from '$utils/discord/events'
|
||||
|
||||
import type { TextBasedChannel } from 'discord.js'
|
||||
|
||||
withContext(on, 'interactionCreate', async (context, interaction) => {
|
||||
const {
|
||||
logger,
|
||||
config: { messageScan: msConfig },
|
||||
} = context
|
||||
|
||||
if (!msConfig?.humanCorrections) return
|
||||
if (!interaction.isStringSelectMenu()) return
|
||||
if (!interaction.customId.startsWith('tr_')) return
|
||||
|
||||
const [, channelId, msgId] = interaction.customId.split('_') as ['tr', string, string]
|
||||
if (!channelId || !msgId) return
|
||||
|
||||
try {
|
||||
const channel = (await interaction.client.channels.fetch(channelId)) as TextBasedChannel | null
|
||||
const msg = await channel?.messages.fetch(msgId)
|
||||
|
||||
if (!msg)
|
||||
return void (await interaction.reply({
|
||||
embeds: [
|
||||
createErrorEmbed(
|
||||
'Message not found',
|
||||
'Thank you for your contribution! Unfortunately, the message could not be found.',
|
||||
),
|
||||
],
|
||||
ephemeral: true,
|
||||
}))
|
||||
|
||||
const selectedLabel = interaction.values[0]!
|
||||
await context.api.client.trainMessage(msg.content, selectedLabel)
|
||||
await interaction.reply({
|
||||
embeds: [
|
||||
createSuccessEmbed(
|
||||
'Message being trained',
|
||||
`Thank you for your contribution! The selected message is being trained as \`${selectedLabel}\`. 🎉`,
|
||||
),
|
||||
],
|
||||
ephemeral: true,
|
||||
})
|
||||
} catch (e) {
|
||||
logger.error('Failed to handle train message interaction:', e)
|
||||
await interaction.reply({
|
||||
embeds: [createStackTraceEmbed(e)],
|
||||
ephemeral: true,
|
||||
})
|
||||
}
|
||||
})
|
||||
Reference in New Issue
Block a user