feat(bots/discord): framework changes and new features

- Migrated to a new command framework which looks better and works better
- Fixed commands not being bundled correctly
- Added message (prefix) commands with argument validation
- Added a new CommandErrorType, for invalid arguments
- `/eval` is now a bit safer
- Corrected colors for the coinflip embed
- `/stop` now works even when the bot is not connected to the API
This commit is contained in:
PalmDevs
2024-07-30 21:05:12 +07:00
parent a848a9c896
commit 646ec8da87
36 changed files with 1153 additions and 616 deletions

View File

@@ -1,9 +1,5 @@
import { type Response, responses } from '$/database/schemas'
import type {
Config,
ConfigMessageScanResponse,
ConfigMessageScanResponseLabelConfig
} from 'config.schema'
import type { Config, ConfigMessageScanResponse, ConfigMessageScanResponseLabelConfig } from 'config.schema'
import type { Message, PartialUser, User } from 'discord.js'
import { eq } from 'drizzle-orm'
import { createMessageScanResponseEmbed } from './embeds'
@@ -17,7 +13,7 @@ export const getResponseFromText = async (
): Promise<ConfigMessageScanResponse & { label?: string }> => {
let responseConfig: Awaited<ReturnType<typeof getResponseFromText>> = {
triggers: {},
response: null
response: null,
}
const firstLabelIndexes: number[] = []
@@ -29,7 +25,7 @@ export const getResponseFromText = async (
// Filter override check is not neccessary here, we are already passing responses that match the filter
// from the messageCreate handler, see line 17 of messageCreate handler
const {
triggers: { text: textTriggers, image: imageTriggers }
triggers: { text: textTriggers, image: imageTriggers },
} = trigger
if (responseConfig) break
@@ -92,7 +88,7 @@ export const getResponseFromText = async (
logger.debug('No match from NLP, doing after regexes')
for (let i = 0; i < responses.length; i++) {
const {
triggers: { text: textTriggers }
triggers: { text: textTriggers },
} = responses[i]!
const firstLabelIndex = firstLabelIndexes[i] ?? -1
@@ -113,10 +109,7 @@ export const getResponseFromText = async (
return responseConfig
}
export const messageMatchesFilter = (
message: Message,
filter: NonNullable<Config['messageScan']>['filter'],
) => {
export const messageMatchesFilter = (message: Message, filter: NonNullable<Config['messageScan']>['filter']) => {
if (!filter) return true
const memberRoles = new Set(message.member?.roles.cache.keys())
@@ -124,7 +117,12 @@ export const messageMatchesFilter = (
// If matches blacklist, will return false
// Any other case, will return true
return !(blFilter && (blFilter.channels?.includes(message.channelId) || blFilter.roles?.some(role => memberRoles.has(role)) || blFilter.users?.includes(message.author.id)))
return !(
blFilter &&
(blFilter.channels?.includes(message.channelId) ||
blFilter.roles?.some(role => memberRoles.has(role)) ||
blFilter.users?.includes(message.author.id))
)
}
export const handleUserResponseCorrection = async (

View File

@@ -1,6 +1,6 @@
import { config, logger } from '$/context'
import decancer from 'decancer'
import type { ChatInputCommandInteraction, EmbedBuilder, Guild, GuildMember, User } from 'discord.js'
import type { ChatInputCommandInteraction, EmbedBuilder, Guild, GuildMember, Message, User } from 'discord.js'
import { applyReferenceToModerationActionEmbed, createModerationActionEmbed } from './embeds'
const PresetLogAction = {
@@ -10,19 +10,23 @@ const PresetLogAction = {
export const sendPresetReplyAndLogs = (
action: keyof typeof PresetLogAction,
interaction: ChatInputCommandInteraction,
interaction: ChatInputCommandInteraction | Message,
executor: GuildMember,
user: User,
preset: string,
expires?: number | null,
) =>
sendModerationReplyAndLogs(
interaction,
createModerationActionEmbed(PresetLogAction[action], user, interaction.user, undefined, expires, [
createModerationActionEmbed(PresetLogAction[action], user, executor.user, undefined, expires, [
[{ name: 'Preset', value: preset, inline: true }],
]),
)
export const sendModerationReplyAndLogs = async (interaction: ChatInputCommandInteraction, embed: EmbedBuilder) => {
export const sendModerationReplyAndLogs = async (
interaction: ChatInputCommandInteraction | Message,
embed: EmbedBuilder,
) => {
const reply = await interaction.reply({ embeds: [embed] }).then(it => it.fetch())
const logChannel = await getLogChannel(interaction.guild!)
await logChannel?.send({ embeds: [applyReferenceToModerationActionEmbed(embed, reply.url)] })
@@ -46,7 +50,7 @@ export const getLogChannel = async (guild: Guild) => {
}
export const cureNickname = async (member: GuildMember) => {
if (!member.manageable) throw new Error('Member is not manageable')
if (!member.manageable) return
const name = member.displayName
let cured = decancer(name)
.toString()

View File

@@ -2,10 +2,13 @@ import { GuildMember, type User } from 'discord.js'
import config from '../../../config'
export const isAdmin = (userOrMember: User | GuildMember) => {
return config.admin?.users?.includes(userOrMember.id) || (userOrMember instanceof GuildMember && isMemberAdmin(userOrMember))
return (
config.admin?.users?.includes(userOrMember.id) ||
(userOrMember instanceof GuildMember && isMemberAdmin(userOrMember))
)
}
export const isMemberAdmin = (member: GuildMember) => {
const roles = new Set(member.roles.cache.keys())
return Boolean(config?.admin?.roles?.[member.guild.id]?.some(role => roles.has(role)))
}
}

View File

@@ -6,9 +6,9 @@ import { and, eq } from 'drizzle-orm'
// TODO: Fix this type
type PresetKey = string
export const applyRolePreset = async (member: GuildMember, presetName: PresetKey, untilMs: number | null) => {
export const applyRolePreset = async (member: GuildMember, presetName: PresetKey, untilMs: number) => {
const afterInsert = await applyRolesUsingPreset(presetName, member, true)
const until = untilMs ? Math.ceil(untilMs / 1000) : null
const until = untilMs === Infinity ? null : Math.ceil(untilMs / 1000)
await database
.insert(appliedPresets)

View File

@@ -7,17 +7,27 @@ export const listAllFilesRecursive = (dir: string): string[] =>
.filter(x => x.isFile())
.map(x => join(x.parentPath, x.name).replaceAll(pathSep, posixPathSep))
export const generateCommandsIndex = (dirPath: string) => generateIndexes(dirPath, x => !x.endsWith('types.ts'))
export const generateCommandsIndex = (dirPath: string) =>
generateIndexes(dirPath, (x, i) => `export { default as C${i} } from './${x}'`)
export const generateEventsIndex = (dirPath: string) => generateIndexes(dirPath)
const generateIndexes = async (dirPath: string, pathFilter?: (path: string) => boolean) => {
const generateIndexes = async (
dirPath: string,
customMap?: (path: string, index: number) => string,
pathFilter?: (path: string) => boolean,
) => {
const files = listAllFilesRecursive(dirPath)
.filter(x => (x.endsWith('.ts') && !x.endsWith('index.ts') && pathFilter ? pathFilter(x) : true))
.filter(x => x.endsWith('.ts') && !x.endsWith('index.ts') && (pathFilter ? pathFilter(x) : true))
.map(x => relative(dirPath, x).replaceAll(pathSep, posixPathSep))
writeFileSync(
join(dirPath, 'index.ts'),
`// AUTO-GENERATED BY A SCRIPT, DON'T TOUCH\n\n${files.map(c => `import './${c.split('.').at(-2)}'`).join('\n')}`,
`// AUTO-GENERATED BY A SCRIPT, DON'T TOUCH\n\n${files
.map((c, i) => {
const path = c.split('.').at(-2)!
return customMap ? customMap(path, i) : `import './${path}'`
})
.join('\n')}`,
)
}