mirror of
https://github.com/ReVanced/revanced-bots.git
synced 2026-01-22 18:53:57 +00:00
feat(bots/discord): add source
This commit is contained in:
15
bots/discord/src/utils/api/events.ts
Normal file
15
bots/discord/src/utils/api/events.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import type { ClientWebSocketEvents } from '@revanced/bot-api'
|
||||
import { api } from '../../context'
|
||||
|
||||
const { client } = api
|
||||
|
||||
export function on<Event extends EventName>(event: Event, listener: ListenerOf<Event>) {
|
||||
client.on(event, listener)
|
||||
}
|
||||
|
||||
export function once<Event extends EventName>(event: Event, listener: ListenerOf<Event>) {
|
||||
client.once(event, listener)
|
||||
}
|
||||
|
||||
export type EventName = keyof ClientWebSocketEvents
|
||||
export type ListenerOf<Event extends EventName> = ClientWebSocketEvents[Event]
|
||||
19
bots/discord/src/utils/discord/commands.ts
Normal file
19
bots/discord/src/utils/discord/commands.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import type { Command } from '$commands'
|
||||
import { listAllFilesRecursive } from '$utils/fs'
|
||||
|
||||
export const loadCommands = async () => {
|
||||
const commandsMap: Record<string, Command> = {}
|
||||
const files = await listAllFilesRecursive('src/commands')
|
||||
const commands = await Promise.all(
|
||||
files.map(async file => {
|
||||
const command = await import(file)
|
||||
return command.default
|
||||
}),
|
||||
)
|
||||
|
||||
for (const command of commands) {
|
||||
if (command) commandsMap[command.data.name] = command
|
||||
}
|
||||
|
||||
return commandsMap
|
||||
}
|
||||
48
bots/discord/src/utils/discord/embeds.ts
Normal file
48
bots/discord/src/utils/discord/embeds.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { DefaultEmbedColor, ReVancedLogoURL } from '$/constants'
|
||||
import { EmbedBuilder } from 'discord.js'
|
||||
import type { ConfigMessageScanResponseMessage } from '../../../config.example'
|
||||
|
||||
export const createErrorEmbed = (title: string, description?: string) =>
|
||||
applyCommonStyles(
|
||||
new EmbedBuilder()
|
||||
.setTitle(title)
|
||||
.setDescription(description ?? null)
|
||||
.setAuthor({ name: 'Error' })
|
||||
.setColor('Red'),
|
||||
false,
|
||||
)
|
||||
|
||||
export const createStackTraceEmbed = (stack: unknown) =>
|
||||
// biome-ignore lint/style/useTemplate: shut
|
||||
createErrorEmbed('An exception was thrown', '```js' + stack + '```')
|
||||
|
||||
export const createSuccessEmbed = (title: string, description?: string) =>
|
||||
applyCommonStyles(
|
||||
new EmbedBuilder()
|
||||
.setTitle(title)
|
||||
.setDescription(description ?? null)
|
||||
.setAuthor({ name: 'Success' })
|
||||
.setColor('Green'),
|
||||
false,
|
||||
)
|
||||
|
||||
export const createMessageScanResponseEmbed = (response: ConfigMessageScanResponseMessage) => {
|
||||
const embed = new EmbedBuilder().setTitle(response.title)
|
||||
|
||||
if (response.description) embed.setDescription(response.description)
|
||||
if (response.fields) embed.addFields(response.fields)
|
||||
|
||||
return applyCommonStyles(embed)
|
||||
}
|
||||
|
||||
const applyCommonStyles = (embed: EmbedBuilder, setColor = true, setThumbnail = true) => {
|
||||
embed.setFooter({
|
||||
text: 'ReVanced',
|
||||
iconURL: ReVancedLogoURL,
|
||||
})
|
||||
|
||||
if (setColor) embed.setColor(DefaultEmbedColor)
|
||||
if (setThumbnail) embed.setThumbnail(ReVancedLogoURL)
|
||||
|
||||
return embed
|
||||
}
|
||||
19
bots/discord/src/utils/discord/events.ts
Normal file
19
bots/discord/src/utils/discord/events.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import * as context from '$/context'
|
||||
import type { ClientEvents } from 'discord.js'
|
||||
|
||||
const { client } = context.discord
|
||||
|
||||
export const on = <Event extends EventName>(event: Event, listener: ListenerOf<Event>) =>
|
||||
client.on(event, (...args) => listener(context, ...args))
|
||||
|
||||
export const once = <Event extends EventName>(event: Event, listener: ListenerOf<Event>) =>
|
||||
client.once(event, (...args) => listener(context, ...args))
|
||||
|
||||
export type EventName = keyof ClientEvents
|
||||
export type EventMap = {
|
||||
[K in EventName]: ListenerOf<K>
|
||||
}
|
||||
|
||||
type ListenerOf<Event extends EventName> = (
|
||||
...args: [typeof import('$/context'), ...ClientEvents[Event]]
|
||||
) => void | Promise<void>
|
||||
140
bots/discord/src/utils/discord/messageScan.ts
Normal file
140
bots/discord/src/utils/discord/messageScan.ts
Normal file
@@ -0,0 +1,140 @@
|
||||
import type { LabeledResponse } from '$/classes/Database'
|
||||
import type { Config, ConfigMessageScanResponseLabelConfig, ConfigMessageScanResponseMessage } from 'config.example'
|
||||
import type { Message, PartialUser, User } from 'discord.js'
|
||||
import { createMessageScanResponseEmbed } from './embeds'
|
||||
|
||||
export const getResponseFromContent = async (
|
||||
content: string,
|
||||
{ api, logger, config: { messageScan: config } }: typeof import('src/context'),
|
||||
) => {
|
||||
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 (const trigger of config.responses) {
|
||||
const { triggers, response: resp } = trigger
|
||||
if (response) break
|
||||
|
||||
for (let i = 0; i < triggers.length; i++) {
|
||||
const trigger = triggers[i]!
|
||||
|
||||
if (trigger instanceof RegExp) {
|
||||
if (trigger.test(content)) {
|
||||
logger.debug(`Message matched regex (before mode): ${trigger.source}`)
|
||||
response = resp
|
||||
break
|
||||
}
|
||||
} else {
|
||||
firstLabelIndexes.push(i)
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If none of the regexes match, we can search for labels immediately
|
||||
if (!response) {
|
||||
const scan = await api.client.parseText(content)
|
||||
if (scan.labels.length) {
|
||||
const matchedLabel = scan.labels[0]!
|
||||
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(
|
||||
(x): x is ConfigMessageScanResponseLabelConfig => 'label' in x && x.label === matchedLabel.name,
|
||||
)
|
||||
if (config) triggerConfig = config
|
||||
return config
|
||||
})
|
||||
|
||||
if (!labelConfig) {
|
||||
logger.warn(`No label config found for label ${matchedLabel.name}`)
|
||||
return { response: null, label: undefined }
|
||||
}
|
||||
|
||||
if (matchedLabel.confidence >= triggerConfig!.threshold) {
|
||||
logger.debug('Label confidence is enough')
|
||||
label = matchedLabel.name
|
||||
response = labelConfig.response
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we still don't have a label, we can match all regexes after the initial label trigger
|
||||
if (!response)
|
||||
for (let i = 0; i < config.responses.length; i++) {
|
||||
const { triggers, response: resp } = config.responses[i]!
|
||||
const firstLabelIndex = firstLabelIndexes[i] ?? -1
|
||||
|
||||
for (let i = firstLabelIndex + 1; i < triggers.length; i++) {
|
||||
const trigger = triggers[i]!
|
||||
|
||||
if (trigger instanceof RegExp) {
|
||||
if (trigger.test(content)) {
|
||||
logger.debug(`Message matched regex (after mode): ${trigger.source}`)
|
||||
response = resp
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
response,
|
||||
label,
|
||||
}
|
||||
}
|
||||
|
||||
export const shouldScanMessage = (
|
||||
message: Message,
|
||||
config: NonNullable<Config['messageScan']>,
|
||||
): message is Message<true> => {
|
||||
if (message.author.bot) return false
|
||||
if (!message.guild) return false
|
||||
|
||||
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),
|
||||
]
|
||||
|
||||
if (config.whitelist && filters.every(x => !x)) return false
|
||||
if (!config.whitelist && filters.some(x => x)) return false
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const handleUserResponseCorrection = async (
|
||||
{ api, database: db, config: { messageScan: msConfig }, logger }: typeof import('$/context'),
|
||||
response: LabeledResponse,
|
||||
reply: Message,
|
||||
label: string,
|
||||
user: User | PartialUser,
|
||||
) => {
|
||||
const correctLabelResponse = msConfig!.responses!.find(r => r.triggers.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())
|
||||
|
||||
if (response.label !== label) {
|
||||
db.labeledResponses.edit(response.reply, { label, correctedBy: user.id })
|
||||
await reply.edit({
|
||||
embeds: [createMessageScanResponseEmbed(correctLabelResponse.response)],
|
||||
})
|
||||
}
|
||||
|
||||
await api.client.trainMessage(response.text, label)
|
||||
logger.debug(`User ${user.id} trained message ${response.reply} as ${label} (positive)`)
|
||||
|
||||
await reply.reactions.removeAll()
|
||||
}
|
||||
14
bots/discord/src/utils/discord/security.ts
Normal file
14
bots/discord/src/utils/discord/security.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import type { Guild, GuildManager } from 'discord.js'
|
||||
import { config, logger } from '../../context'
|
||||
|
||||
export function leaveDisallowedGuild(guild: Guild) {
|
||||
logger.warn(`Server ${guild.name} (${guild.id}) is not allowed to use this bot.`)
|
||||
return guild.leave().then(() => logger.debug(`Left guild ${guild.name} (${guild.id})`))
|
||||
}
|
||||
|
||||
export async function leaveDisallowedGuilds(guildManager: GuildManager) {
|
||||
const guilds = await guildManager.fetch()
|
||||
for (const [id, guild] of guilds) {
|
||||
if (!config.allowedGuilds.includes(id)) await leaveDisallowedGuild(await guild.fetch())
|
||||
}
|
||||
}
|
||||
17
bots/discord/src/utils/fs.ts
Normal file
17
bots/discord/src/utils/fs.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import { join } from 'path'
|
||||
import { readdir, stat } from 'fs/promises'
|
||||
|
||||
export async function listAllFilesRecursive(dir: string): Promise<string[]> {
|
||||
const files = await readdir(dir)
|
||||
const result: string[] = []
|
||||
for (const file of files) {
|
||||
const filePath = join(dir, file)
|
||||
const fileStat = await stat(filePath)
|
||||
if (fileStat.isDirectory()) {
|
||||
result.push(...(await listAllFilesRecursive(filePath)))
|
||||
} else {
|
||||
result.push(filePath)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user