b68/packages/bot/src/events/message-create.ts

457 lines
17 KiB
TypeScript

import type { Message, PermissionResolvable, TextChannel } from 'discord.js'
import { ownerId } from '../../config/bot.json'
import type { TextCommand } from '../sturctures/command'
import type { GuildConfig } from '../sturctures/database'
import type { DiscordEvent } from '../sturctures/event'
import { cooldownCache } from '../utils/cache'
import { getCommandHelpInfo, resembleCommandCheck } from '../utils/cmds'
import { isDev } from '../utils/constants'
import { parseMsToVisibleText } from '../utils/formatters'
import { callbackEmbed } from '../utils/messages'
import MYGuildConfig from '../utils/guild.config'
async function reject(message: Message, usage: string, missing: string) {
const postMessage = await message.reply({
content: `Usage: \`${usage}\`\nMissing: \`${missing}\``,
allowedMentions: { repliedUser: true },
})
setTimeout(() => {
if (postMessage.deletable) postMessage.delete().catch(() => undefined)
}, 6000)
}
export const event: DiscordEvent = {
name: 'messageCreate',
// eslint-disable-next-line
run: async (message: Message) => {
const { content, channel, author, webhookId, member, guild, client } =
message
if (
author.bot ||
channel.isVoiceBased() ||
webhookId ||
author.id === client.user.id
)
return
const prefix = 'd!'
if (guild) {
if (!member) await guild.members.fetch(author.id)
}
const guildDatabase: GuildConfig = MYGuildConfig
const mentionReg = new RegExp(`^(<@!?${client.user.id}>)`)
const mentionTest = mentionReg.test(content)
if (mentionTest) {
await channel.send(`Hey! My prefix is \`${prefix}\``)
return
}
const prefixReg = new RegExp(`^${prefix}`)
const prefixTest = content.match(prefixReg)
if (prefixTest) {
const [prefix] = prefixTest
const parsedContent = content.slice(prefix.length)
if (!parsedContent) return
const command = parsedContent.split(' ')[0]
// Command Content Parsing.
let cmd: TextCommand | undefined
let cmdName = command
// Fetch command destination.
for (let index = 0; index < 2; index++) {
const commandMatching = client.commands.get(cmdName)
const aliasesMatching = client.aliases.get(cmdName)
if (commandMatching) {
cmd = commandMatching
cmdName = cmd.data.name
break
} else if (aliasesMatching) {
cmd = client.commands.get(aliasesMatching)
if (cmd) {
cmdName = cmd.data.name
}
break
} else {
if (index === 0) {
const expectedCommandName = await resembleCommandCheck(
message,
cmdName
)
if (expectedCommandName) {
cmdName = expectedCommandName.name
message.createdTimestamp +=
expectedCommandName.timeTaken
continue
}
}
return
}
}
// Reject if no.
if (!cmd) return
const cmdData = cmd.data
/**
* Command's eligibility vaildation.
*/
if (cmd.enabled === false) return
// Reject if not in development mode
if (cmdData.developmentOnly === true && !isDev) {
return
}
if (cmdData.ownerOnly === true && author.id !== ownerId) return
// Reject if dm mode while configurated to guild.
if (!guild && !cmdData.directMessageAllowed) return
// Reject if command can executed NSFW channel when it's not in.
if (
cmdData.nsfwChannelRequired &&
(!guild || !channel.isTextBased()) &&
!(channel as TextChannel).nsfw
) {
const cEmbed = callbackEmbed({
text: 'You must be in **NSFW** channel before executing this commmand.',
color: 'Red',
mode: 'error',
})
author
.send({
embeds: [cEmbed],
})
.catch(() => undefined)
return
}
// Reject if dm mode while configurated to guild.
if (!guild && !cmdData.directMessageAllowed) return
// Reject when Target disabled or didn't pass.
if (guild) {
const commandDatasbase = guildDatabase?.commands
// GUILD specifies disabled command.
if (
!commandDatasbase ||
commandDatasbase.global.disabled.includes(cmdName)
) {
return
}
// Specified CATAGORIES
if (
cmd.data.catagory &&
commandDatasbase.global.disabledCatagories.includes(
cmd.data.catagory
)
) {
return
}
// Specified CHANNEL
if (
commandDatasbase.channelDisabled
.find((x) => x.id === channel.id)
?.cmds.includes(cmdName)
) {
author
.send({
content: `Command cannot be executed in this channel (#${channel.id})!`,
allowedMentions: { repliedUser: true },
})
.catch(() => undefined)
return
}
// Specified ROLE
if (member?.roles.cache) {
// eslint-disable-next-line no-unsafe-optional-chaining
for (const role of member.roles.cache) {
const hasRole = commandDatasbase.roleDisabled
.find((x) => x.id === role[1].id)
?.cmds.includes(cmdName)
if (hasRole) return
}
}
// Specified USER
if (
commandDatasbase.userDisabled
.find((x) => x.id === author.id)
?.cmds.includes(cmdName)
) {
author
.send({
content:
'You are disabled from executing this command!',
allowedMentions: { repliedUser: true },
})
.catch(() => undefined)
return
}
if (
cmd.data.inVoiceChannelRequired === true &&
!member?.voice.channel
) {
const cEmbed = callbackEmbed({
text: 'You must be in voice channel before executing this commmand.',
color: 'Red',
mode: 'error',
})
await message.reply({
embeds: [cEmbed],
})
return
}
}
/**
* END of Command's eligibility vaildation.
*/
// Cooldown Validation
const now = Date.now()
const keyName = `CMD_${author.id}_${cmdName}`
const cooldownInterval = cmd.data.cooldownInterval ?? 3000
// Reject if user exists in cooldown.
if (cooldownCache.has(keyName)) {
const expectedEnd = cooldownCache.get(keyName)
if (expectedEnd && now < Number(expectedEnd)) {
const timeleft = parseMsToVisibleText(
Number(expectedEnd) - now
)
const postMessage = await message.reply({
content: `Before using this command, please wait for **${timeleft}**.`,
allowedMentions: { repliedUser: true },
})
setTimeout(() => {
if (postMessage.deletable)
postMessage.delete().catch(() => undefined)
}, 6000)
return
}
}
// Set cooldown.
cooldownCache.set(
keyName,
now + cooldownInterval,
cooldownInterval / 1000
)
// Reject if excess usage.
if (cmd.data.intervalLimit) {
const key1 = 'INTERVAL' + keyName
let doRejection = { is: false, which: '' }
const customTTL = {
minute: 60 * 1000,
hour: 60 * 60 * 1000,
day: 24 * 60 * 60 * 1000,
}
const intervalList = cmd.data.intervalLimit
for (const [key, ms] of Object.entries(intervalList)) {
if (!ms) continue
const keyTyped = key as keyof typeof intervalList
if (!intervalList[keyTyped]) continue
const userFeq = cooldownCache.get(keyTyped + key1) ?? '0'
// Do Rejection if number reached specified amount of allowed cooldown.
if (Number(userFeq) === intervalList[keyTyped]) {
doRejection = { is: true, which: keyTyped }
break
}
// Set to Database with TTL.
cooldownCache.set(
keyTyped + key1,
(Number(userFeq) + 1).toString(),
customTTL[keyTyped]
)
}
if (doRejection.is) {
const postMessage = await message.reply({
content: `You have reached the maxmium usage in 1 **${doRejection.which}**!`,
allowedMentions: { repliedUser: true },
})
setTimeout(() => {
if (postMessage.deletable)
postMessage.delete().catch(() => undefined)
}, 6000)
return
}
}
// Permission Check (BOT)
const requestPermsClient = cmd.data.clientRequiredPermissions
if (guild && requestPermsClient) {
const permMissing: PermissionResolvable[] = []
for (const perm of requestPermsClient) {
const botId = client.user.id
if (botId) {
const isOwned = guild.members.cache
.get(botId)
?.permissions.has(perm)
if (!isOwned) permMissing.push(perm)
}
}
// Reject if BOT doesn't own permission(s).
if (permMissing.length > 0) {
const perms = permMissing
.map((index) => `\`${Number(index)}\``)
.join(', ')
await message.reply({
content: `I don't have **PERMISSIONS**: ${perms}`,
})
return
}
}
// Permission Check (AUTHOR)
const requestPermsAuthor = cmd.data.authorRequiredPermissions
if (guild && requestPermsAuthor) {
const permMissing: PermissionResolvable[] = []
for (const perm of requestPermsAuthor) {
const isOwned = member?.permissions.has(perm)
if (!isOwned) permMissing.push(perm)
}
// Reject if AUTHOR doesn't own permission(s).
if (permMissing.length > 0) {
const perms = permMissing
.map((index) => `\`${Number(index)}\``)
.join(', ')
await message.reply({
content: `You do not have required **PERMISSIONS**: ${perms}`,
})
return
}
}
// Pass
console.log(
`[CMD] ${author.tag} executed ${cmdName} in ${
guild?.name ?? 'DM'
}.`
)
const arguments_ = parsedContent.split(' ').slice(1)
if (arguments_[0] === 'help') {
if (
cmd.data.nsfwChannelRequired === true &&
(channel as TextChannel).nsfw
) {
const helpInfo = getCommandHelpInfo(cmd)
await message.reply({
embeds: [helpInfo],
})
}
return
}
// Arguments Checking
const requiredArugments = cmd.data.requiredArgs
if (requiredArugments && requiredArugments.length > 0) {
let usage = `${prefix}${cmd.data.name}`
for (const _argument_ of requiredArugments) {
let namedArguments: string = _argument_.type
if (_argument_.type === 'STRING' && _argument_.text) {
namedArguments = _argument_.text.join(' | ')
} else if (_argument_.name) {
namedArguments += `(${_argument_.name})`
}
usage += ` [${namedArguments}]`
}
for (
let index = 0, l = requiredArugments.length;
index < l;
index++
) {
const _argument = requiredArugments[index]
const userArgument = arguments_[index]
switch (_argument.type) {
case 'STRING': {
if (_argument.required) {
if (
!userArgument ||
userArgument.length === 0
) {
return reject(
message,
usage,
index.toString()
)
}
if (
_argument.text &&
!_argument.text.includes(userArgument)
) {
const text = `${index.toString()} (NOT_MATCH)`
return reject(message, usage, text)
}
if (
_argument.customLength &&
(content.length >
_argument.customLength.max! ||
content.length <
_argument.customLength.min!)
) {
const text = `${index.toString()} (ERR_Length)`
return reject(message, usage, text)
}
}
break
}
case 'NUMBER': {
if (
_argument.required &&
Number.isNaN(Number(userArgument))
) {
return reject(message, usage, index.toString())
}
break
}
default: {
break
}
}
if (_argument.rest) break
}
}
// Run the actual command.
try {
return cmd.run({ message, args: arguments_ })
} catch (error) {
if (error instanceof Error) console.error(error)
}
}
},
}