mirror of
https://github.com/ReVanced/revanced-bots.git
synced 2026-01-28 13:41:02 +00:00
feat(bots/discord)!: read commit description
FEATURES: - Updated documentation - Improved configuration format - Allow filter overriding for each response config (closes #29) - Improved commands directory structure - Improved slash command reload script - New commands - New command exception handling
This commit is contained in:
31
bots/discord/src/classes/CommandError.ts
Normal file
31
bots/discord/src/classes/CommandError.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { createErrorEmbed } from '$/utils/discord/embeds'
|
||||
|
||||
export default class CommandError extends Error {
|
||||
type: CommandErrorType
|
||||
|
||||
constructor(type: CommandErrorType, message?: string) {
|
||||
super(message)
|
||||
this.name = 'CommandError'
|
||||
this.type = type
|
||||
}
|
||||
|
||||
toEmbed() {
|
||||
return createErrorEmbed(ErrorTitleMap[this.type], this.message ?? '')
|
||||
}
|
||||
}
|
||||
|
||||
export enum CommandErrorType {
|
||||
Generic,
|
||||
MissingArgument,
|
||||
InvalidUser,
|
||||
InvalidChannel,
|
||||
InvalidDuration,
|
||||
}
|
||||
|
||||
const ErrorTitleMap: Record<CommandErrorType, string> = {
|
||||
[CommandErrorType.Generic]: 'An exception was thrown',
|
||||
[CommandErrorType.MissingArgument]: 'Missing argument',
|
||||
[CommandErrorType.InvalidUser]: 'Invalid user',
|
||||
[CommandErrorType.InvalidChannel]: 'Invalid channel',
|
||||
[CommandErrorType.InvalidDuration]: 'Invalid duration',
|
||||
}
|
||||
45
bots/discord/src/commands/development/exception-test.ts
Normal file
45
bots/discord/src/commands/development/exception-test.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
import { SlashCommandBuilder } from 'discord.js'
|
||||
|
||||
import CommandError, { CommandErrorType } from '$/classes/CommandError'
|
||||
import type { Command } from '..'
|
||||
|
||||
export default {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('exception-test')
|
||||
.setDescription('throw up pls')
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('type')
|
||||
.setDescription('The type of exception to throw')
|
||||
.addChoices({
|
||||
name: 'generic error',
|
||||
value: 'Generic',
|
||||
})
|
||||
.addChoices({
|
||||
name: 'invalid argument',
|
||||
value: 'InvalidArgument',
|
||||
})
|
||||
.addChoices({
|
||||
name: 'invalid channel',
|
||||
value: 'InvalidChannel',
|
||||
})
|
||||
.addChoices({
|
||||
name: 'invalid user',
|
||||
value: 'InvalidUser',
|
||||
})
|
||||
.addChoices({
|
||||
name: 'invalid duration',
|
||||
value: 'InvalidDuration',
|
||||
})
|
||||
.setRequired(true),
|
||||
)
|
||||
.setDMPermission(true)
|
||||
.toJSON(),
|
||||
|
||||
global: true,
|
||||
|
||||
async execute(_, interaction) {
|
||||
const type = interaction.options.getString('type', true)
|
||||
throw new CommandError(CommandErrorType[type as keyof typeof CommandErrorType], '[INTENTIONAL BOT DESIGN]')
|
||||
},
|
||||
} satisfies Command
|
||||
@@ -1,8 +1,9 @@
|
||||
import { SlashCommandBuilder } from 'discord.js'
|
||||
import type { Command } from '.'
|
||||
|
||||
import type { Command } from '..'
|
||||
|
||||
export default {
|
||||
data: new SlashCommandBuilder().setName('stop').setDescription('Stops the bot').toJSON(),
|
||||
data: new SlashCommandBuilder().setName('stop').setDescription('Stops the bot').setDMPermission(true).toJSON(),
|
||||
|
||||
ownerOnly: true,
|
||||
global: true,
|
||||
34
bots/discord/src/commands/fun/coinflip.ts
Normal file
34
bots/discord/src/commands/fun/coinflip.ts
Normal file
@@ -0,0 +1,34 @@
|
||||
import { applyCommonEmbedStyles } from '$/utils/discord/embeds'
|
||||
|
||||
import { EmbedBuilder, SlashCommandBuilder } from 'discord.js'
|
||||
|
||||
import type { Command } from '..'
|
||||
|
||||
export default {
|
||||
data: new SlashCommandBuilder().setName('coinflip').setDescription('Do a coinflip!').setDMPermission(true).toJSON(),
|
||||
global: true,
|
||||
|
||||
async execute(_, interaction) {
|
||||
const result = Math.random() < 0.5 ? ('heads' as const) : ('tails' as const)
|
||||
const embed = applyCommonEmbedStyles(new EmbedBuilder().setTitle('Flipping... 🪙'), true, false, false)
|
||||
|
||||
await interaction.reply({
|
||||
embeds: [embed.toJSON()],
|
||||
})
|
||||
|
||||
embed.setTitle(`The coin landed on... **${result.toUpperCase()}**! ${EmojiMap[result]}`)
|
||||
|
||||
setTimeout(
|
||||
() =>
|
||||
interaction.editReply({
|
||||
embeds: [embed.toJSON()],
|
||||
}),
|
||||
1500,
|
||||
)
|
||||
},
|
||||
} satisfies Command
|
||||
|
||||
const EmojiMap: Record<'heads' | 'tails', string> = {
|
||||
heads: '🤯',
|
||||
tails: '🐈',
|
||||
}
|
||||
43
bots/discord/src/commands/fun/reply.ts
Normal file
43
bots/discord/src/commands/fun/reply.ts
Normal file
@@ -0,0 +1,43 @@
|
||||
import { SlashCommandBuilder, type TextBasedChannel } from 'discord.js'
|
||||
|
||||
import type { Command } from '..'
|
||||
|
||||
export default {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('reply')
|
||||
.setDescription('Send a message as the bot')
|
||||
.addStringOption(option => option.setName('message').setDescription('The message to send').setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('reference')
|
||||
.setDescription('The message ID to reply to (use `latest` to reply to the latest message)')
|
||||
.setRequired(false),
|
||||
)
|
||||
.toJSON(),
|
||||
|
||||
memberRequirements: {
|
||||
roles: ['955220417969262612', '973886585294704640'],
|
||||
},
|
||||
|
||||
global: false,
|
||||
|
||||
async execute({ logger }, interaction) {
|
||||
const msg = interaction.options.getString('message', true)
|
||||
const ref = interaction.options.getString('reference')
|
||||
|
||||
const channel = (await interaction.guild!.channels.fetch(interaction.channelId)) as TextBasedChannel
|
||||
const refMsg = ref?.startsWith('latest') ? (await channel.messages.fetch({ limit: 1 })).at(0)?.id : ref
|
||||
|
||||
await channel.send({
|
||||
content: msg,
|
||||
reply: refMsg ? { messageReference: refMsg, failIfNotExists: true } : undefined,
|
||||
})
|
||||
|
||||
logger.info(`User ${interaction.user.tag} made the bot say: ${msg}`)
|
||||
|
||||
await interaction.reply({
|
||||
content: 'OK!',
|
||||
ephemeral: true,
|
||||
})
|
||||
},
|
||||
} satisfies Command
|
||||
@@ -22,7 +22,6 @@ export type Command = {
|
||||
mode?: 'all' | 'any'
|
||||
/**
|
||||
* The permissions required to use this command (in BitFields).
|
||||
* For safety reasons, this is set to `-1n` and only bot owners can use this command unless explicitly specified.
|
||||
*
|
||||
* - **0n** means that everyone can use this command.
|
||||
* - **-1n** means that only bot owners can use this command.
|
||||
@@ -37,13 +36,12 @@ export type Command = {
|
||||
}
|
||||
/**
|
||||
* Whether this command can only be used by bot owners.
|
||||
* For safety reasons, this is set to `true` and only bot owners can use this command unless explicitly specified.
|
||||
* @default true
|
||||
* @default false
|
||||
*/
|
||||
ownerOnly?: boolean
|
||||
/**
|
||||
* Whether to register this command as a global slash command.
|
||||
* For safety reasons, this is set to `false` and commands will be registered in allowed guilds only.
|
||||
* This is set to `false` and commands will be registered in allowed guilds only by default.
|
||||
* @default false
|
||||
*/
|
||||
global?: boolean
|
||||
|
||||
58
bots/discord/src/commands/moderation/slowmode.ts
Normal file
58
bots/discord/src/commands/moderation/slowmode.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { createSuccessEmbed } from '$/utils/discord/embeds'
|
||||
import { durationToString, parseDuration } from '$/utils/duration'
|
||||
|
||||
import { SlashCommandBuilder } from 'discord.js'
|
||||
|
||||
import CommandError, { CommandErrorType } from '$/classes/CommandError'
|
||||
import type { Command } from '..'
|
||||
|
||||
export default {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('slowmode')
|
||||
.setDescription('Set a slowmode for the current channel')
|
||||
.addStringOption(option => option.setName('duration').setDescription('The duration to set').setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('channel')
|
||||
.setDescription('The channel to set the slowmode on (defaults to current channel)')
|
||||
.setRequired(false),
|
||||
)
|
||||
.toJSON(),
|
||||
|
||||
memberRequirements: {
|
||||
roles: ['955220417969262612', '973886585294704640'],
|
||||
},
|
||||
|
||||
global: false,
|
||||
|
||||
async execute({ logger }, interaction) {
|
||||
const durationStr = interaction.options.getString('duration', true)
|
||||
const id = interaction.options.getChannel('channel')?.id ?? interaction.channelId
|
||||
|
||||
const duration = parseDuration(durationStr)
|
||||
const channel = await interaction.guild!.channels.fetch(id)
|
||||
|
||||
if (!channel?.isTextBased())
|
||||
throw new CommandError(
|
||||
CommandErrorType.InvalidChannel,
|
||||
'The supplied channel is not a text channel or does not exist.',
|
||||
)
|
||||
|
||||
if (Number.isNaN(duration)) throw new CommandError(CommandErrorType.InvalidDuration, 'Invalid duration.')
|
||||
if (duration < 0 || duration > 36e4)
|
||||
throw new CommandError(
|
||||
CommandErrorType.InvalidDuration,
|
||||
'Duration out of range, must be between 0s and 6h.',
|
||||
)
|
||||
|
||||
logger.info(`Setting slowmode to ${duration}ms on ${channel.id}`)
|
||||
|
||||
await channel.setRateLimitPerUser(
|
||||
duration / 1000,
|
||||
`Slowmode set by @${interaction.user.username} (${interaction.user.id})`,
|
||||
)
|
||||
await interaction.reply({
|
||||
embeds: [createSuccessEmbed(`Slowmode set to ${durationToString(duration)} on ${channel.toString()}`)],
|
||||
})
|
||||
},
|
||||
} satisfies Command
|
||||
@@ -1,57 +0,0 @@
|
||||
import { createStackTraceEmbed } from '$/utils/discord/embeds'
|
||||
import { PermissionFlagsBits, SlashCommandBuilder, type TextBasedChannel } from 'discord.js'
|
||||
import type { Command } from '.'
|
||||
|
||||
export default {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('reply')
|
||||
.setDescription('Send a message as the bot')
|
||||
.addStringOption(option => option.setName('message').setDescription('The message to send').setRequired(true))
|
||||
.addStringOption(option =>
|
||||
option
|
||||
.setName('reference')
|
||||
.setDescription('The message ID to reply to (use `latest` to reply to the latest message)')
|
||||
.setRequired(false),
|
||||
)
|
||||
.toJSON(),
|
||||
|
||||
memberRequirements: {
|
||||
mode: 'all',
|
||||
roles: ['955220417969262612', '973886585294704640'],
|
||||
permissions: PermissionFlagsBits.ManageMessages,
|
||||
},
|
||||
|
||||
global: false,
|
||||
|
||||
async execute({ logger }, interaction) {
|
||||
const msg = interaction.options.getString('message', true)
|
||||
const ref = interaction.options.getString('reference')
|
||||
|
||||
const resolvedRef = ref?.startsWith('latest')
|
||||
? (await interaction.channel?.messages.fetch({ limit: 1 }))?.at(0)?.id
|
||||
: ref
|
||||
|
||||
try {
|
||||
const channel = (await interaction.guild!.channels.fetch(interaction.channelId)) as TextBasedChannel | null
|
||||
if (!channel) throw new Error('Channel not found (or not cached)')
|
||||
|
||||
await channel.send({
|
||||
content: msg,
|
||||
reply: {
|
||||
messageReference: resolvedRef!,
|
||||
failIfNotExists: true,
|
||||
},
|
||||
})
|
||||
|
||||
logger.warn(`User ${interaction.user.tag} made the bot say: ${msg}`)
|
||||
await interaction.reply({
|
||||
content: 'OK!',
|
||||
ephemeral: true,
|
||||
})
|
||||
} catch (e) {
|
||||
await interaction.reply({
|
||||
embeds: [createStackTraceEmbed(e)],
|
||||
})
|
||||
}
|
||||
},
|
||||
} satisfies Command
|
||||
@@ -1,3 +1,4 @@
|
||||
import CommandError from '$/classes/CommandError'
|
||||
import { createErrorEmbed, createStackTraceEmbed } from '$utils/discord/embeds'
|
||||
import { on } from '$utils/discord/events'
|
||||
|
||||
@@ -8,40 +9,46 @@ export default on('interactionCreate', async (context, interaction) => {
|
||||
const command = discord.commands[interaction.commandName]
|
||||
|
||||
logger.debug(`Command ${interaction.commandName} being invoked by ${interaction.user.tag}`)
|
||||
if (!command) return void logger.error(`Command ${interaction.commandName} not implemented but registered!!!`)
|
||||
|
||||
if (!command) {
|
||||
logger.error(`Command ${interaction.commandName} not implemented but registered!!!`)
|
||||
return void interaction.reply({
|
||||
embeds: [
|
||||
createErrorEmbed(
|
||||
'Command not implemented',
|
||||
'This command has not been implemented yet. Please report this to the developers.',
|
||||
),
|
||||
],
|
||||
ephemeral: true,
|
||||
})
|
||||
}
|
||||
const isOwner = config.owners.includes(interaction.user.id)
|
||||
|
||||
const userIsOwner = config.owners.includes(interaction.user.id)
|
||||
|
||||
if ((command.ownerOnly ?? true) && !userIsOwner)
|
||||
/**
|
||||
* Owner check
|
||||
*/
|
||||
if (command.ownerOnly && !isOwner)
|
||||
return void (await interaction.reply({
|
||||
embeds: [createErrorEmbed('Massive skill issue', 'This command can only be used by the bot owners.')],
|
||||
ephemeral: true,
|
||||
}))
|
||||
|
||||
/**
|
||||
* Sanity check
|
||||
*/
|
||||
if (!command.global && !interaction.inGuild()) {
|
||||
logger.error(`Command ${interaction.commandName} cannot be run in DMs, but was registered as global`)
|
||||
await interaction.reply({
|
||||
embeds: [createErrorEmbed('Cannot run that here', 'This command can only be used in a server.')],
|
||||
ephemeral: true,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
/**
|
||||
* Permission checks
|
||||
*/
|
||||
if (interaction.inGuild()) {
|
||||
// Bot owners get bypass
|
||||
if (command.memberRequirements && !userIsOwner) {
|
||||
const { permissions = -1n, roles = [], mode } = command.memberRequirements
|
||||
if (command.memberRequirements && !isOwner) {
|
||||
const { permissions = 0n, roles = [], mode } = command.memberRequirements
|
||||
|
||||
const member = await interaction.guild!.members.fetch(interaction.user.id)
|
||||
|
||||
const [missingPermissions, missingRoles] = [
|
||||
// This command is an owner-only command (the user is not an owner, checked above)
|
||||
permissions < 0n ||
|
||||
permissions <= 0n ||
|
||||
// or the user doesn't have the required permissions
|
||||
(permissions >= 0n && !interaction.memberPermissions.has(permissions)),
|
||||
(permissions > 0n && !interaction.memberPermissions.has(permissions)),
|
||||
|
||||
// If not:
|
||||
!roles.some(x => member.roles.cache.has(x)),
|
||||
@@ -66,7 +73,7 @@ export default on('interactionCreate', async (context, interaction) => {
|
||||
} catch (err) {
|
||||
logger.error(`Error while executing command ${interaction.commandName}:`, err)
|
||||
await interaction.reply({
|
||||
embeds: [createStackTraceEmbed(err)],
|
||||
embeds: [err instanceof CommandError ? err.toEmbed() : createStackTraceEmbed(err)],
|
||||
ephemeral: true,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { MessageScanLabeledResponseReactions } from '$/constants'
|
||||
import { getResponseFromContent, shouldScanMessage } from '$/utils/discord/messageScan'
|
||||
import { getResponseFromText, shouldScanMessage } from '$/utils/discord/messageScan'
|
||||
import { createMessageScanResponseEmbed } from '$utils/discord/embeds'
|
||||
import { on } from '$utils/discord/events'
|
||||
|
||||
@@ -12,35 +12,41 @@ on('messageCreate', async (ctx, msg) => {
|
||||
} = ctx
|
||||
|
||||
if (!config || !config.responses) return
|
||||
if (!shouldScanMessage(msg, config)) return
|
||||
|
||||
const filteredResponses = config.responses.filter(x => shouldScanMessage(msg, x.filterOverride ?? config.filter))
|
||||
if (!filteredResponses.length) return
|
||||
|
||||
if (msg.content.length) {
|
||||
logger.debug(`Classifying message ${msg.id}`)
|
||||
try {
|
||||
logger.debug(`Classifying message ${msg.id}`)
|
||||
|
||||
const { response, label } = await getResponseFromContent(msg.content, ctx)
|
||||
const { response, label } = await getResponseFromText(msg.content, filteredResponses, ctx)
|
||||
|
||||
if (response) {
|
||||
logger.debug('Response found')
|
||||
if (response) {
|
||||
logger.debug('Response found')
|
||||
|
||||
const reply = await msg.reply({
|
||||
embeds: [createMessageScanResponseEmbed(response, label ? 'nlp' : 'match')],
|
||||
})
|
||||
|
||||
if (label)
|
||||
db.labeledResponses.save({
|
||||
reply: reply.id,
|
||||
channel: reply.channel.id,
|
||||
guild: reply.guild.id,
|
||||
referenceMessage: msg.id,
|
||||
label,
|
||||
text: msg.content,
|
||||
const reply = await msg.reply({
|
||||
embeds: [createMessageScanResponseEmbed(response, label ? 'nlp' : 'match')],
|
||||
})
|
||||
|
||||
if (label) {
|
||||
for (const reaction of Object.values(MessageScanLabeledResponseReactions)) {
|
||||
await reply.react(reaction)
|
||||
if (label)
|
||||
db.labeledResponses.save({
|
||||
reply: reply.id,
|
||||
channel: reply.channel.id,
|
||||
guild: reply.guild!.id,
|
||||
referenceMessage: msg.id,
|
||||
label,
|
||||
text: msg.content,
|
||||
})
|
||||
|
||||
if (label) {
|
||||
for (const reaction of Object.values(MessageScanLabeledResponseReactions)) {
|
||||
await reply.react(reaction)
|
||||
}
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error('Failed to classify message:', e)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,7 +58,7 @@ on('messageCreate', async (ctx, msg) => {
|
||||
|
||||
try {
|
||||
const { text: content } = await api.client.parseImage(attachment.url)
|
||||
const { response } = await getResponseFromContent(content, ctx, true)
|
||||
const { response } = await getResponseFromText(content, filteredResponses, ctx, true)
|
||||
|
||||
if (response) {
|
||||
logger.debug(`Response found for attachment: ${attachment.url}`)
|
||||
|
||||
@@ -30,27 +30,31 @@ on('messageReactionAdd', async (context, rct, user) => {
|
||||
if (reactionMessage.author.id !== reaction.client.user!.id) return
|
||||
if (!PossibleReactions.includes(reaction.emoji.name!)) return
|
||||
|
||||
if (reactionMessage.inGuild() && msConfig.humanCorrections.memberRequirements) {
|
||||
const {
|
||||
memberRequirements: { roles, permissions },
|
||||
} = msConfig.humanCorrections
|
||||
|
||||
if (!roles && !permissions)
|
||||
return void logger.warn(
|
||||
'No member requirements specified for human corrections, ignoring this request for security reasons',
|
||||
)
|
||||
|
||||
const member = await reactionMessage.guild.members.fetch(user.id)
|
||||
|
||||
if (!config.owners.includes(user.id)) {
|
||||
// User is in guild, and config has member requirements
|
||||
if (
|
||||
permissions &&
|
||||
!member.permissions.has(permissions) &&
|
||||
roles &&
|
||||
!roles.some(role => member.roles.cache.has(role))
|
||||
)
|
||||
return
|
||||
// User is not owner, and not included in allowUsers
|
||||
} else if (!config.owners.includes(user.id) && !msConfig.humanCorrections.allowUsers?.includes(user.id)) return
|
||||
reactionMessage.inGuild() &&
|
||||
(msConfig.humanCorrections.allow?.members || msConfig.humanCorrections.allow?.users)
|
||||
) {
|
||||
const {
|
||||
allow: { users: allowedUsers, members: allowedMembers },
|
||||
} = msConfig.humanCorrections
|
||||
|
||||
if (allowedMembers) {
|
||||
const member = await reactionMessage.guild.members.fetch(user.id)
|
||||
const { permissions, roles } = allowedMembers
|
||||
|
||||
if (!(member.permissions.has(permissions ?? 0n) || roles?.some(role => member.roles.cache.has(role))))
|
||||
return
|
||||
} else if (allowedUsers) {
|
||||
if (!allowedUsers.includes(user.id)) return
|
||||
} else {
|
||||
return void logger.warn(
|
||||
'No member or user requirements set for human corrections, all requests will be ignored',
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Sanity check
|
||||
const response = db.labeledResponses.get(rct.message.id)
|
||||
@@ -69,7 +73,9 @@ on('messageReactionAdd', async (context, rct, user) => {
|
||||
// Bot is wrong :(
|
||||
|
||||
const labels = msConfig.responses!.flatMap(r =>
|
||||
r.triggers.filter((t): t is ConfigMessageScanResponseLabelConfig => 'label' in t).map(t => t.label),
|
||||
r.triggers
|
||||
.text!.filter((t): t is ConfigMessageScanResponseLabelConfig => 'label' in t)
|
||||
.map(t => t.label),
|
||||
)
|
||||
|
||||
const componentPrefix = `cr_${reactionMessage.id}`
|
||||
|
||||
@@ -1,55 +1,59 @@
|
||||
import type { LabeledResponse } from '$/classes/Database'
|
||||
import type { Config, ConfigMessageScanResponseLabelConfig, ConfigMessageScanResponseMessage } from 'config.example'
|
||||
import type {
|
||||
Config,
|
||||
ConfigMessageScanResponse,
|
||||
ConfigMessageScanResponseLabelConfig,
|
||||
ConfigMessageScanResponseMessage,
|
||||
} from 'config.example'
|
||||
import type { Message, PartialUser, User } from 'discord.js'
|
||||
import { createMessageScanResponseEmbed } from './embeds'
|
||||
|
||||
export const getResponseFromContent = async (
|
||||
export const getResponseFromText = async (
|
||||
content: string,
|
||||
{ api, logger, config: { messageScan: config } }: typeof import('src/context'),
|
||||
responses: ConfigMessageScanResponse[],
|
||||
// Just to be safe that we will never use data from the context parameter
|
||||
{ api, logger }: Omit<typeof import('src/context'), 'config'>,
|
||||
ocrMode = false,
|
||||
) => {
|
||||
if (!config || !config.responses) {
|
||||
logger.warn('No message scan config found')
|
||||
|
||||
return {
|
||||
response: null,
|
||||
label: undefined,
|
||||
}
|
||||
}
|
||||
|
||||
let label: string | undefined
|
||||
let response: ConfigMessageScanResponseMessage | undefined | null
|
||||
const firstLabelIndexes: number[] = []
|
||||
|
||||
// Test if all regexes before a label trigger is matched
|
||||
for (let i = 0; i < config.responses.length; i++) {
|
||||
const trigger = config.responses[i]!
|
||||
for (let i = 0; i < responses.length; i++) {
|
||||
const trigger = responses[i]!
|
||||
|
||||
const { triggers, ocrTriggers, response: resp } = trigger
|
||||
// Filter override check is not neccessary here, we are already passing responses that match the filter
|
||||
// from the messageCreate handler
|
||||
const {
|
||||
triggers: { text: textTriggers, image: imageTriggers },
|
||||
response: resp,
|
||||
} = trigger
|
||||
if (response) break
|
||||
|
||||
if (ocrMode && ocrTriggers)
|
||||
for (const regex of ocrTriggers)
|
||||
if (regex.test(content)) {
|
||||
logger.debug(`Message matched regex (OCR mode): ${regex.source}`)
|
||||
response = resp
|
||||
if (ocrMode) {
|
||||
if (imageTriggers)
|
||||
for (const regex of imageTriggers)
|
||||
if (regex.test(content)) {
|
||||
logger.debug(`Message matched regex (OCR mode): ${regex.source}`)
|
||||
response = resp
|
||||
break
|
||||
}
|
||||
} else
|
||||
for (let j = 0; j < textTriggers!.length; j++) {
|
||||
const trigger = textTriggers![j]!
|
||||
|
||||
if (trigger instanceof RegExp) {
|
||||
if (trigger.test(content)) {
|
||||
logger.debug(`Message matched regex (before mode): ${trigger.source}`)
|
||||
response = resp
|
||||
break
|
||||
}
|
||||
} else {
|
||||
firstLabelIndexes[i] = j
|
||||
break
|
||||
}
|
||||
|
||||
for (let j = 0; j < triggers.length; j++) {
|
||||
const trigger = triggers[j]!
|
||||
|
||||
if (trigger instanceof RegExp) {
|
||||
if (trigger.test(content)) {
|
||||
logger.debug(`Message matched regex (before mode): ${trigger.source}`)
|
||||
response = resp
|
||||
break
|
||||
}
|
||||
} else {
|
||||
firstLabelIndexes[i] = j
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the regexes match, we can search for labels immediately
|
||||
@@ -61,8 +65,8 @@ export const getResponseFromContent = async (
|
||||
logger.debug(`Message matched label with confidence: ${matchedLabel.name}, ${matchedLabel.confidence}`)
|
||||
|
||||
let triggerConfig: ConfigMessageScanResponseLabelConfig | undefined
|
||||
const labelConfig = config.responses.find(x => {
|
||||
const config = x.triggers.find(
|
||||
const labelConfig = responses.find(x => {
|
||||
const config = x.triggers.text!.find(
|
||||
(x): x is ConfigMessageScanResponseLabelConfig => 'label' in x && x.label === matchedLabel.name,
|
||||
)
|
||||
if (config) triggerConfig = config
|
||||
@@ -85,12 +89,15 @@ export const getResponseFromContent = async (
|
||||
// If we still don't have a label, we can match all regexes after the initial label trigger
|
||||
if (!response) {
|
||||
logger.debug('No match from NLP, doing after regexes')
|
||||
for (let i = 0; i < config.responses.length; i++) {
|
||||
const { triggers, response: resp } = config.responses[i]!
|
||||
for (let i = 0; i < responses.length; i++) {
|
||||
const {
|
||||
triggers: { text: textTriggers },
|
||||
response: resp,
|
||||
} = responses[i]!
|
||||
const firstLabelIndex = firstLabelIndexes[i] ?? -1
|
||||
|
||||
for (let i = firstLabelIndex + 1; i < triggers.length; i++) {
|
||||
const trigger = triggers[i]!
|
||||
for (let i = firstLabelIndex + 1; i < textTriggers!.length; i++) {
|
||||
const trigger = textTriggers![i]!
|
||||
|
||||
if (trigger instanceof RegExp) {
|
||||
if (trigger.test(content)) {
|
||||
@@ -111,19 +118,20 @@ export const getResponseFromContent = async (
|
||||
|
||||
export const shouldScanMessage = (
|
||||
message: Message,
|
||||
config: NonNullable<Config['messageScan']>,
|
||||
filter: NonNullable<Config['messageScan']>['filter'],
|
||||
): message is Message<true> => {
|
||||
if (message.author.bot) return false
|
||||
if (!message.guild) return false
|
||||
if (!filter) return true
|
||||
|
||||
const filters = [
|
||||
config.users?.includes(message.author.id),
|
||||
message.member?.roles.cache.some(x => config.roles?.includes(x.id)),
|
||||
config.channels?.includes(message.channel.id),
|
||||
filter.users?.includes(message.author.id),
|
||||
message.member?.roles.cache.some(x => filter.roles?.includes(x.id)),
|
||||
filter.channels?.includes(message.channel.id),
|
||||
]
|
||||
|
||||
if (config.whitelist && filters.every(x => !x)) return false
|
||||
if (!config.whitelist && filters.some(x => x)) return false
|
||||
if (filter.whitelist && filters.every(x => !x)) return false
|
||||
if (!filter.whitelist && filters.some(x => x)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
@@ -135,7 +143,9 @@ export const handleUserResponseCorrection = async (
|
||||
label: string,
|
||||
user: User | PartialUser,
|
||||
) => {
|
||||
const correctLabelResponse = msConfig!.responses!.find(r => r.triggers.some(t => 'label' in t && t.label === label))
|
||||
const correctLabelResponse = msConfig!.responses!.find(r =>
|
||||
r.triggers.text!.some(t => 'label' in t && t.label === label),
|
||||
)
|
||||
|
||||
if (!correctLabelResponse) throw new Error('Cannot find label config for the selected label')
|
||||
if (!correctLabelResponse.response) return void (await reply.delete())
|
||||
|
||||
Reference in New Issue
Block a user