feat: Added Discord Bot && Added Upload Service

This commit is contained in:
Jyotirmoy Bandyopadhayaya 2023-05-12 20:56:58 +05:30
parent cf39c03ba3
commit 27ff15a8aa
Signed by: bravo68web
GPG Key ID: F5671FD7BCB9917A
64 changed files with 3350 additions and 71 deletions

View File

@ -48,6 +48,19 @@ export interface IConfigKeys {
KEYCLOAK_REDIRECT_URI: string
KEYCLOAK_AUTH_SERVER_URL: string
KEYCLOAK_REALM: string
R2_CLIENT_ID: string
R2_CLIENT_SECRET: string
R2_BUCKET_NAME: string
R2_BUCKET_REGION: string
R2_BUCKET_ENDPOINT: string
R2_BUCKET_URL: string
R2_BUCKET_FOLDER: string
DISCORD_BOT_TOKEN: string
DISCORD_BOT_CLIENT_ID: number
DISCORD_BOT_CLIENT_SECRET: string
DISCORD_SERVER_ID: number
DISCORD_WEBHOOK_URL: string
DISCORD_SELF_ID: number
}
export default class ConfigStoreFactory {

View File

@ -3,7 +3,7 @@ import { Request, Response } from 'express'
import { makeResponse } from '../libs'
export default class DiscordController extends DiscordService {
public getProfile = async (req: Request, res: Response) => {
public getActivity = async (req: Request, res: Response) => {
try {
const data = await this.activity()
res.send(makeResponse(data))
@ -22,4 +22,22 @@ export default class DiscordController extends DiscordService {
res.send(makeResponse(error.message, {}, 'Failed', true))
}
}
public getProfile = async (req: Request, res: Response) => {
try {
const data = await this.profile()
res.send(makeResponse(data))
} catch (error: any) {
res.send(makeResponse(error.message, {}, 'Failed', true))
}
}
public getPresence = async (req: Request, res: Response) => {
try {
const data = await this.presence()
res.send(makeResponse(data))
} catch (error: any) {
res.send(makeResponse(error.message, {}, 'Failed', true))
}
}
}

View File

@ -0,0 +1,27 @@
import { NextFunction, Request, Response } from 'express'
import { ModRequest } from '../types'
import Uploader from '../services/upload.service'
export default class UploadController extends Uploader {
public upload = async (
req: ModRequest,
res: Response,
next: NextFunction
) => {
try {
const { file } = req
if (!file) {
const error = new Error('Please upload a file')
next(error)
}
const data = await this.uploadS(file)
res.status(200).json({
success: true,
message: 'File uploaded successfully',
data,
})
} catch (error: any) {
next(error)
}
}
}

View File

@ -0,0 +1,67 @@
import { S3Client, PutObjectCommand, S3ClientConfig } from '@aws-sdk/client-s3'
import { configKeys } from '..'
type IUploaderServerType = 's3' | 'r2' | 'safe'
export default class UploaderService {
private static _s3Client
private static _s3Opts
private static _clientType
constructor(bucket, clientType: IUploaderServerType = 's3') {
const options = {
bucket,
}
UploaderService._s3Opts = options
UploaderService._clientType = clientType
if (clientType === 's3') {
const s3ClientOpts: S3ClientConfig = {
region: configKeys.S3_BUCKET_REGION || '',
endpoint: configKeys.S3_BUCKET_ENDPOINT || '',
forcePathStyle: true,
credentials: {
accessKeyId: configKeys.S3_CLIENT_ID || '',
secretAccessKey: configKeys.S3_CLIENT_SECRET || '',
},
}
const client = new S3Client(s3ClientOpts)
UploaderService._s3Client = client
} else if (clientType === 'r2') {
const s3ClientOpts: S3ClientConfig = {
region: configKeys.R2_BUCKET_REGION || '',
endpoint: configKeys.R2_BUCKET_ENDPOINT || '',
forcePathStyle: true,
credentials: {
accessKeyId: configKeys.R2_CLIENT_ID || '',
secretAccessKey: configKeys.R2_CLIENT_SECRET || '',
},
}
const client = new S3Client(s3ClientOpts)
UploaderService._s3Client = client
}
}
async uploadFile(
entity: string,
id: string,
file: Buffer,
fileType: string,
acl?: string
) {
const key = [entity, id].join('/')
const uploadParams = {
Bucket: UploaderService._s3Opts.bucket,
ACL: acl,
ContentType: fileType,
Body: file,
Key: key,
}
await UploaderService._s3Client.send(new PutObjectCommand(uploadParams))
return {
url: configKeys.S3_BUCKET_URL,
bucket_name: configKeys.S3_BUCKET_NAME,
folder: configKeys.S3_BUCKET_FOLDER,
}
}
}

View File

@ -0,0 +1,54 @@
import { REST } from '@discordjs/rest'
import {
WebSocketManager,
WebSocketShardStatus,
WebSocketShardEvents,
} from '@discordjs/ws'
import {
GatewayDispatchEvents,
GatewayIntentBits,
Client,
} from '@discordjs/core'
import { configKeys } from '..'
export default class DiscordBotClient {
public static _client: Client
private static _rest: REST
public static _gateway: WebSocketManager
private static _currentPresence
public static init() {
this._rest = new REST({ version: '10' }).setToken(
configKeys.DISCORD_BOT_TOKEN
)
this._gateway = new WebSocketManager({
token: configKeys.DISCORD_BOT_TOKEN,
intents: GatewayIntentBits.GuildPresences,
rest: this._rest,
shardCount: 1,
shardIds: [0],
})
this._client = new Client({
rest: this._rest,
gateway: this._gateway,
})
}
public static getPresence = async () => {
return this._currentPresence
}
// public static getShard = async () => {
// return this._gateway.on(WebSocketShardEvents.Dispatch, (data) => {
// console.log(data)
// })
// }
public static setPresence = async (presence: any) => {
this._currentPresence = presence
}
public static getUser = async (id: number) => {
return this._client.rest.get(`/users/${id}`)
}
}

View File

@ -0,0 +1,29 @@
import { GatewayDispatchEvents } from '@discordjs/core'
import DiscordBotClient from './discord_bot.factory'
import { configKeys } from '..'
export default async () => {
DiscordBotClient.init()
const DiscordBot = DiscordBotClient._client
DiscordBot.on(GatewayDispatchEvents.PresenceUpdate, async ({ data }) => {
if (data.user.id == String(configKeys.DISCORD_SELF_ID)) {
DiscordBotClient.setPresence(data)
}
})
const selfInfo = await DiscordBot.api.users.getCurrent()
DiscordBot.once(GatewayDispatchEvents.Ready, async () => {
console.log(
'🔮 ' +
selfInfo.username +
'#' +
selfInfo.discriminator +
' : Gateway Connected!'
)
})
DiscordBotClient._gateway.connect()
}

View File

@ -1,82 +1,43 @@
import { S3Client } from '@aws-sdk/client-s3'
import multer from 'multer'
import multerS3 from 'multer-s3'
import path from 'path'
import napiNanoId from 'napi-nanoid'
import { configKeys } from '..'
interface UploadFactoryOptions {
region: string
bucket: string
accessKey: string
secretKey: string
}
import { nanoid } from 'napi-nanoid'
interface UploaderConfig {
folder: string
mimeFilters: string[]
}
export class UploadFactory {
private options: UploadFactoryOptions & Partial<UploaderConfig>
private s3Client: S3Client
constructor(options?: Partial<UploadFactoryOptions>) {
this.options = {
bucket: options?.bucket || configKeys.S3_BUCKET_NAME || '',
region: options?.region || configKeys.S3_BUCKET_REGION || '',
accessKey: options?.accessKey || configKeys.S3_CLIENT_ID || '',
secretKey: options?.secretKey || configKeys.S3_CLIENT_SECRET || '',
}
this.s3Client = new S3Client({
region: this.options.region,
credentials: {
accessKeyId: this.options.accessKey,
secretAccessKey: this.options.secretKey,
public getUploader(config?: UploaderConfig) {
const storage = multer.memoryStorage({
filename: (req, file, cb) => {
const fileName =
file.originalname +
'-' +
nanoid() +
path.extname(file.originalname)
cb(null, fileName)
},
})
}
public get serviceName(): string {
return 'aws:' + this.options.bucket
}
public getUploader(
config?: Partial<UploadFactoryOptions & UploaderConfig>
) {
const finalOptions = {
...this.options,
...(config || {}),
}
return multer({
fileFilter(_req, file, cb) {
const res = finalOptions.mimeFilters
? finalOptions.mimeFilters.includes(file.mimetype)
: true
cb(null, res)
},
storage: multerS3({
s3: this.s3Client,
bucket: this.options.bucket,
acl: 'public-read',
contentType: multerS3.AUTO_CONTENT_TYPE,
metadata: function (_req, file, cb) {
const meta = {
fieldName: file.fieldname,
fileName: file.originalname,
uploadOn: new Date().toISOString(),
storage: storage,
fileFilter: (req, file, cb) => {
const fileName =
file.originalname.split('.')[
file.originalname.split('.').length - 2
] +
'-' +
nanoid() +
path.extname(file.originalname)
file.newName = fileName
if (config?.mimeFilters?.length) {
if (config.mimeFilters.includes(file.mimetype)) {
cb(null, true)
} else {
cb(new Error('File type not allowed'), false)
}
cb(null, meta)
},
key: function (_req, file, cb) {
const key: string[] = []
if (finalOptions.folder) key.push(finalOptions.folder)
const value = napiNanoId.nanoid()
const ext = path.extname(file.originalname)
key.push(value + ext)
cb(null, key.join('/'))
},
}),
} else {
cb(null, true)
}
},
})
}
}

View File

@ -10,6 +10,7 @@ import routes from './routes'
import { errorHandler, notFoundHandler } from './libs'
import pkg from './package.json' assert { type: 'json' }
import configStore, { IConfigKeys } from './configs'
import discordBotConnect from './helpers/discord_bot_client'
export const app: express.Application = express()
@ -23,6 +24,8 @@ console.log(isDev ? '🚀 Production Mode' : '👷 Development Mode')
const configs = new configStore(isDev)
const configKeys: IConfigKeys = (await configs.getConfigStore()) as IConfigKeys
discordBotConnect()
app.use(cors())
app.use(helmet())
app.use(morgan('dev'))

View File

@ -10,6 +10,8 @@
"private": true,
"dependencies": {
"@aws-sdk/client-s3": "^3.226.0",
"@discordjs/core": "^0.6.0",
"@discordjs/rest": "^1.7.1",
"@types/express": "^4.17.14",
"axios": "^1.2.1",
"cors": "^2.8.5",

View File

@ -0,0 +1,23 @@
import { UploadFactory } from '../../helpers/upload.factory'
import { Router } from 'express'
import UploadController from '../../controllers/upload.controller'
const router = Router()
const uploadController = new UploadController()
const uploaderFactory = new UploadFactory()
router.post(
'/',
uploaderFactory
.getUploader({
mimeFilters: [
'image/jpeg',
'image/png',
'image/gif',
'application/json',
],
})
.single('file'),
uploadController.upload as any
)
export default router

View File

@ -2,9 +2,13 @@ import { Router } from 'express'
import DiscordController from '../../controllers/discord.controller'
const router = Router()
const { getProfile, getBanner } = new DiscordController()
const { getProfile, getBanner, getActivity, getPresence } =
new DiscordController()
router.get('/profile', getProfile)
router.get('/profile', getActivity)
router.get('/banner', getBanner)
router.get('/v2/profile', getProfile)
router.get('/v2/activity', getPresence)
export default router

View File

@ -1,4 +1,6 @@
import { configKeys } from '..'
import axios from '../helpers/axios_client'
import DiscordBotClient from '../helpers/discord_bot.factory'
export default class DiscordService {
public activity = async () => {
@ -14,4 +16,12 @@ export default class DiscordService {
)
return data
}
public profile = async () => {
return DiscordBotClient.getUser(configKeys.DISCORD_SELF_ID)
}
public presence = async () => {
return DiscordBotClient.getPresence()
}
}

View File

@ -0,0 +1,26 @@
import UploaderService from '../data/uploader.service'
import { configKeys } from '..'
const uploaderService = new UploaderService(configKeys.S3_BUCKET_NAME, 'r2')
export default class Uploader {
public uploadS = async (file: any) => {
await uploaderService.uploadFile(
configKeys.S3_BUCKET_FOLDER,
file.newName,
file.buffer,
file.mimetype,
'public-read'
)
return {
url:
configKeys.S3_BUCKET_URL +
'/' +
configKeys.S3_BUCKET_NAME +
'/' +
configKeys.S3_BUCKET_FOLDER +
'/' +
file.newName,
}
}
}

View File

@ -10,4 +10,5 @@ export interface PaginationType {
export interface ModRequest extends Request {
user: any
file: any
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
packages/bot/assets/rip.jpg Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

View File

@ -0,0 +1,8 @@
{
"defaultPrefix": "d!",
"disabledCommandCatagories": [],
"githubLink": "https://github.com/cktsun1031/DraconianBot",
"name": "Hanako",
"ownerId": "457039372009865226",
"useShards": false
}

View File

@ -0,0 +1,5 @@
{
"error": "❌",
"success": "✅",
"warning": "⚠️"
}

View File

@ -0,0 +1,47 @@
import { GuildConfig } from '../src/sturctures/database'
const MYGuildConfig: GuildConfig = {
prefix: 'd!',
serverId: '636830997790326794',
commands: {
global: {
disabled: [],
disabledCatagories: [],
customCooldown: [],
},
userDisabled: [],
roleDisabled: [],
channelDisabled: [],
},
antiSpam: {
enabled: true,
whitelistedUsers: [],
whitelistedRoles: [],
whitelistedChannels: [],
inviteLinks: {
enabled: true,
whitelistedUsers: [],
whitelistedRoles: [],
whitelistedChannels: [],
},
mentions: {
enabled: true,
maxmiumCheck: {
enabled: true,
value: 5,
},
publicRoleCheck: true,
whitelistedUsers: [],
whitelistedRoles: [],
whitelistedChannels: [],
},
},
thread: {
listen: true,
},
snipe: {
channelDisabled: [],
},
}
export default MYGuildConfig

48
packages/bot/package.json Normal file
View File

@ -0,0 +1,48 @@
{
"name": "bot",
"version": "1.0.0",
"description": "Hanako's Egg",
"main": "src/index.ts",
"author": "BRAVO68WEB",
"license": "MIT",
"private": true,
"type": "module",
"devDependencies": {
"@types/crypto-js": "^4.1.1",
"@types/gifencoder": "^2.0.1",
"@types/node": "^20.1.3",
"@types/pidusage": "^2.0.2",
"@types/string-similarity": "^4.0.0",
"nodemon": "^2.0.22",
"ts-node": "^10.9.1",
"typescript": "^5.0.4"
},
"dependencies": {
"@discordjs/core": "^0.6.0",
"@discordjs/rest": "^1.7.1",
"@esbuild-kit/esm-loader": "^2.5.5",
"@types/xml2js": "^0.4.11",
"axios": "^1.4.0",
"canvas": "^2.11.2",
"crypto-js": "^4.1.1",
"dayjs": "^1.11.7",
"discord.js": "^14.11.0",
"dotenv": "^16.0.3",
"express": "^4.18.2",
"gifencoder": "^2.0.1",
"glob": "^10.2.3",
"graphql": "^16.6.0",
"graphql-request": "^6.0.0",
"napi-nanoid": "^0.2.0",
"node-cache": "^5.1.2",
"pidusage": "^3.0.2",
"redis": "^4.6.6",
"string-similarity": "^4.0.4",
"xml2js": "^0.5.0",
"zod": "^3.21.4"
},
"scripts": {
"dev": "nodemon --watch 'src/**/*.ts' --exec node --loader @esbuild-kit/esm-loader src/index.ts",
"start": "node --loader @esbuild-kit/esm-loader src/index.ts"
}
}

51
packages/bot/src/bot.ts Normal file
View File

@ -0,0 +1,51 @@
import { Client, Collection, GatewayIntentBits, Partials } from 'discord.js'
// import './http/server';
import { loadSlashCommand, loadTextCommand } from './loaders/command'
import { loadDiscordEvent } from './loaders/event'
import type { SlashCommand, TextCommand } from './sturctures/command'
import { hgqlInit } from './utils/database'
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.DirectMessages,
GatewayIntentBits.MessageContent,
],
allowedMentions: { parse: ['users', 'roles'], repliedUser: false },
partials: [
Partials.User,
Partials.Channel,
Partials.Message,
Partials.GuildMember,
],
})
client.commands = new Collection()
client.commandsCatagories = new Collection()
client.aliases = new Collection()
client.slashcommands = new Collection()
await loadDiscordEvent(client)
await loadTextCommand(client)
await hgqlInit()
await client.login(process.env.TOKEN)
await loadSlashCommand(client, process.env.CLIENT_ID, process.env.TOKEN)
export default client
// declare types.
declare module 'discord.js' {
export interface Client {
aliases: Collection<string, string>
commands: Collection<string, TextCommand>
slashcommands: Collection<string, SlashCommand>
commandsCatagories: Collection<string, string[]>
}
}

View File

@ -0,0 +1,67 @@
import { EmbedBuilder, version as discordJsVersion } from 'discord.js'
import pidusage from 'pidusage'
import { version as packageVersion } from '../../../../package.json'
import type { TextCommand } from '../../../sturctures/command'
import { parseMsToVisibleText } from '../../../utils/formatters'
export const command: TextCommand = {
data: {
name: 'botinfo',
description: 'Check Bot information.',
directMessageAllowed: true,
},
run: async ({ message }) => {
const apiDelayMS = Math.round(message.client.ws.ping)
const osStats = await pidusage(process.pid)
const embed = new EmbedBuilder()
.setTitle("Bot's Information")
.setDescription(
'Hello! I am Draconian Bot, honored to see you here. Information below is my body analysis :)'
)
.addFields([
{
name: 'Version',
value: `\`${packageVersion}\``,
inline: true,
},
{
name: 'Discord.js',
value: `\`${discordJsVersion}\``,
inline: true,
},
{
name: 'Node',
value: `\`${process.version}\``,
inline: true,
},
{
name: 'CPU',
value: `\`${Math.round(Number(osStats.cpu.toFixed(2)))}%\``,
inline: true,
},
{
name: 'Memory',
value: `\`${Math.round(
osStats.memory / (1024 * 1024)
)}MB\``,
inline: true,
},
{
name: 'Uptime',
value: `\`${parseMsToVisibleText(message.client.uptime)}\``,
inline: true,
},
{
name: 'Network Delay',
value: `\`${apiDelayMS} ms\``,
inline: true,
},
])
await message.reply({
embeds: [embed],
})
},
}

View File

@ -0,0 +1,194 @@
import dayjs from 'dayjs'
import type { TextChannel, ThreadChannel, VoiceChannel } from 'discord.js'
import { EmbedBuilder } from 'discord.js'
import type { TextCommand } from '../../../sturctures/command'
export const command: TextCommand = {
data: {
name: 'channelinfo',
description: "Check server's channel information.",
directMessageAllowed: false,
},
// eslint-disable-next-line
run: async ({ message, args }) => {
const { guild, channel, mentions, member } = message
if (!guild || !member) return
const targetNameId = args[0]
let targetChannel = mentions.channels.first()
if (!targetChannel && targetNameId) {
const fetchedChannel = guild.channels.cache.get(targetNameId)
if (fetchedChannel) targetChannel = fetchedChannel
else {
const name = String(targetNameId).toLowerCase()
const fetchedChannelByKW = guild.channels.cache.find((ur) =>
ur.name.toLowerCase().includes(name)
)
if (fetchedChannelByKW) targetChannel = fetchedChannelByKW
}
}
if (!targetChannel) targetChannel = channel
const embed = new EmbedBuilder()
if (targetChannel.isTextBased()) {
const textChannel = targetChannel as TextChannel
embed.setTitle(`${textChannel.name}'s information:`).addFields([
{ name: 'ID', value: textChannel.id },
{
// eslint-disable-next-line
name: 'Created on',
// eslint-disable-next-line
value: dayjs(textChannel.createdAt.getTime()).format(
'DD/MM/YYYY'
),
inline: true,
},
])
if (textChannel.parent?.name) {
embed.addFields([
{
name: 'Parent',
value: textChannel.parent.name,
inline: true,
},
])
}
embed.addFields([
{
name: 'Position',
value: textChannel.rawPosition.toString(),
inline: true,
},
{
name: 'NSFW',
value: textChannel.nsfw ? 'YES' : 'NO',
inline: true,
},
{
name: 'Viewable',
value: textChannel.viewable ? 'YES' : 'NO',
inline: true,
},
])
embed.setFooter({
iconURL: guild.iconURL() ?? '',
text: `Shard ID: ${guild.shardId}`,
})
await message.reply({
embeds: [embed],
})
return
}
if (targetChannel.isThread()) {
const voiceChannel = targetChannel as ThreadChannel
embed.setTitle(`${voiceChannel.name}'s information:`).addFields([
{ name: 'ID', value: voiceChannel.id },
{
name: 'Created on',
value: dayjs(voiceChannel.createdAt?.getTime()).format(
'DD/MM/YYYY'
),
inline: true,
},
])
if (voiceChannel.parent?.name) {
embed.addFields([
{
name: 'Parent',
value: voiceChannel.parent.name,
inline: true,
},
])
}
embed.addFields([
{
name: 'Joinable',
value: voiceChannel.joinable ? 'YES' : 'NO',
inline: true,
},
{
name: 'Locked',
value: voiceChannel.locked ? 'YES' : 'NO',
inline: true,
},
])
embed.setFooter({
iconURL: guild.iconURL() ?? '',
text: `Shard ID: ${guild.shardId}`,
})
await message.reply({
embeds: [embed],
})
}
if (targetChannel.isVoiceBased()) {
const voiceChannel = targetChannel as unknown as VoiceChannel
embed.setTitle(`${voiceChannel.name}'s information:`).addFields([
{ name: 'ID', value: voiceChannel.id },
{
name: 'Created on',
value: dayjs(voiceChannel.createdAt.getTime()).format(
'DD/MM/YYYY'
),
inline: true,
},
])
if (voiceChannel.parent?.name) {
embed.addFields([
{
name: 'Parent',
value: voiceChannel.parent.name,
inline: true,
},
])
}
embed.addFields([
{
name: 'Position',
value: voiceChannel.rawPosition.toString(),
inline: true,
},
{
name: 'Joinable',
value: voiceChannel.joinable ? 'YES' : 'NO',
inline: true,
},
{
name: 'Speakable',
value: voiceChannel.speakable ? 'YES' : 'NO',
inline: true,
},
])
embed.setFooter({
iconURL: guild.iconURL() ?? '',
text: `Shard ID: ${guild.shardId}`,
})
await message.reply({
embeds: [embed],
})
}
},
}

View File

@ -0,0 +1,99 @@
import type { TextChannel } from 'discord.js'
import { EmbedBuilder } from 'discord.js'
import config from '../../../../config/bot.json'
import type { TextCommand } from '../../../sturctures/command'
import { getCommandHelpInfo } from '../../../utils/cmds'
import { callbackEmbed } from '../../../utils/messages'
export const command: TextCommand = {
data: {
name: 'help',
description: 'Get information or help from the bot.',
directMessageAllowed: true,
},
// eslint-disable-next-line
run: async ({ message, args }) => {
const embed = new EmbedBuilder()
const { client, channel, guildId } = message
if (args[0]) {
let cmd: TextCommand | undefined
const commandMatching = client.commands.get(args[0])
const aliasesMatching = client.aliases.get(args[0])
// Fetch command destination.
if (commandMatching) {
cmd = commandMatching
} else if (aliasesMatching) {
cmd = client.commands.get(aliasesMatching)
} else {
const cEmbed = callbackEmbed({
text: 'Command requested does not exist!',
color: 'Red',
mode: 'error',
})
await message.reply({
embeds: [cEmbed],
})
return
}
if (cmd) {
const helpInfo = getCommandHelpInfo(cmd)
await message.reply({
embeds: [helpInfo],
})
}
} else {
const commandsCatagories = client.commandsCatagories
embed.setDescription(
`Hello🙋!\nOur source code: [Here](${config.githubLink})\nTurely appreciate that you are supporting us.`
)
for (const catagory of commandsCatagories) {
if (catagory[0].toLocaleLowerCase() === 'nsfw') {
if ((channel as TextChannel).nsfw) {
catagory[0] += ' THIS CHANNEL ONLY'
} else {
continue
}
}
const text = catagory[1]
.map((index: string) => {
let _text: string | undefined
const cmd = client.commands.get(index)
if (
guildId ||
(cmd && cmd.data.directMessageAllowed === true)
) {
_text = `\`${index}\``
}
return _text
})
.filter(Boolean)
.join(', ')
if (text.length > 0) {
embed.addFields([{ name: catagory[0], value: text }])
}
}
const avatarURL = client.user.defaultAvatarURL
embed.setTitle('Bot Assistance Centre').setFooter({
text: `© ${new Date().getFullYear()} ${config.name}`,
iconURL: avatarURL,
})
await message.reply({
embeds: [embed],
})
}
},
}

View File

@ -0,0 +1,27 @@
import { EmbedBuilder } from 'discord.js'
import type { TextCommand } from '../../../sturctures/command'
import { isDev } from '../../../utils/constants'
export const command: TextCommand = {
data: {
name: 'invite',
description: 'Invite me to your server!',
directMessageAllowed: true,
},
run: async ({ message }) => {
const { client } = message
if (isDev || client.application.botPublic) {
const link = `https://discord.com/api/oauth2/authorize?client_id=${client.application.id}&permissions=1636381879799&scope=applications.commands%20bot`
const embed = new EmbedBuilder().setDescription(
`Invite to your server: [HERE](${link})`
)
await message.reply({
embeds: [embed],
})
}
},
}

View File

@ -0,0 +1,23 @@
import { EmbedBuilder } from 'discord.js'
import type { TextCommand } from '../../../sturctures/command'
export const command: TextCommand = {
data: {
name: 'ping',
description: "Check Bot's network delay.",
directMessageAllowed: true,
},
run: async ({ message }) => {
const apiDelayMS = Math.round(message.client.ws.ping)
const messageDelayMS = Date.now() - message.createdTimestamp
const embed = new EmbedBuilder().setDescription(
`Action Delay: \`${messageDelayMS}ms\`\nAPI Delay: \`${apiDelayMS}ms\``
)
await message.reply({
embeds: [embed],
})
},
}

View File

@ -0,0 +1,96 @@
import dayjs from 'dayjs'
import { EmbedBuilder } from 'discord.js'
import type { TextCommand } from '../../../sturctures/command'
export const command: TextCommand = {
data: {
name: 'serverinfo',
description: "Check server's stats and information.",
directMessageAllowed: false,
},
run: async ({ message }) => {
const { guild } = message
if (!guild) return
const owner = await guild.fetchOwner()
const embed = new EmbedBuilder()
.setThumbnail(guild.iconURL() ?? '')
.setTitle(`${guild.name}'s information:`)
.addFields([
{ name: 'Owner', value: owner.user.tag, inline: true },
{
name: 'Created on',
value: dayjs(guild.createdAt.getTime()).format(
'DD/MM/YYYY'
),
inline: true,
},
{
name: 'User Count',
value: guild.memberCount.toString(),
inline: true,
},
{
name: 'Bot Count',
value: guild.members.cache
.filter((mb) => mb.user.bot)
.size.toString(),
inline: true,
},
{
name: 'Roles',
value: guild.members.cache
.filter((mb) => mb.user.bot)
.size.toString(),
inline: true,
},
{
name: 'Roles',
value: guild.roles.cache.size.toString(),
inline: true,
},
{
name: 'Emojis',
value: guild.channels.cache.size.toString(),
inline: true,
},
{
name: 'Verification Level',
value: guild.verificationLevel.toString(),
inline: true,
},
])
.setFooter({
text: `Shard ID: ${guild.shardId}`,
})
if (guild.description) embed.setDescription(guild.description)
if (guild.premiumSubscriptionCount) {
embed.addFields([
{
name: 'Total Boosts',
value: guild.premiumSubscriptionCount.toString(),
inline: true,
},
])
}
if (guild.vanityURLCode) {
const url = `https://discord.gg/${guild.vanityURLCode}`
embed.addFields([
{ name: 'Vanity Invite URL', value: `[${url}](${url})` },
])
}
await message.reply({
embeds: [embed],
})
},
}

View File

@ -0,0 +1,28 @@
import { EmbedBuilder } from 'discord.js'
import type { TextCommand } from '../../../sturctures/command'
import { parseMsToVisibleText } from '../../../utils/formatters'
export const command: TextCommand = {
data: {
name: 'uptime',
description: 'Check Bot uptime duration.',
directMessageAllowed: true,
},
run: async ({ message }) => {
const instanceBoot = message.client.uptime
const bootTimeMS = Math.round((Date.now() - instanceBoot) / 1000)
const embed = new EmbedBuilder()
.setTitle("Bot's Uptime")
.setDescription(
`\`${parseMsToVisibleText(
instanceBoot
)}\`\n**Booted at** <t:${bootTimeMS}>`
)
await message.reply({
embeds: [embed],
})
},
}

View File

@ -0,0 +1,45 @@
import axios from 'axios'
import { EmbedBuilder } from 'discord.js'
import type { TextCommand } from '../../../sturctures/command'
export const command: TextCommand = {
data: {
name: 'cat',
description: 'Fetch cat image.',
directMessageAllowed: true,
cooldownInterval: 6 * 1000,
},
run: async ({ message }) => {
const embed = new EmbedBuilder()
const url = 'https://api.thecatapi.com/v1/images/search?format=json'
try {
const response = await axios.get(url)
const responseData = response.data
const data: {
id: string
url: string
} = responseData[0]
embed
.setTitle('Cat here')
.setImage(data.url)
.setFooter({
text: `ID: ${data.id}`,
})
await message.reply({
embeds: [embed],
})
} catch {
embed.setDescription('Error occured when fetching meme content.')
await message.reply({
embeds: [embed],
})
}
},
}

View File

@ -0,0 +1,40 @@
import axios from 'axios'
import { EmbedBuilder } from 'discord.js'
import type { TextCommand } from '../../../sturctures/command'
export const command: TextCommand = {
data: {
name: 'dog',
description: 'Fetch dog image.',
directMessageAllowed: true,
cooldownInterval: 6 * 1000,
},
run: async ({ message }) => {
const embed = new EmbedBuilder()
const url = 'https://dog.ceo/api/breeds/image/random'
try {
const response = await axios.get(url)
const responseData: {
status: string
message: string
} = response.data
if (responseData.status === 'success') {
embed.setTitle('Dog here').setImage(responseData.message)
await message.reply({
embeds: [embed],
})
}
} catch {
embed.setDescription('Error occured when fetching meme content.')
await message.reply({
embeds: [embed],
})
}
},
}

View File

@ -0,0 +1,44 @@
import axios from 'axios'
import { EmbedBuilder } from 'discord.js'
import type { TextCommand } from '../../../sturctures/command'
export const command: TextCommand = {
data: {
name: 'joke',
description: 'Get some jokes.',
directMessageAllowed: true,
cooldownInterval: 6 * 1000,
},
run: async ({ message }) => {
const embed = new EmbedBuilder()
const url = 'https://v2.jokeapi.dev/joke/Any?type=single'
try {
const response = await axios.get(url)
const responseData: {
id: string
joke: string
error: boolean
category: string
} = response.data
if (responseData.error) throw 0
embed.setTitle(responseData.joke).setFooter({
text: `Category: ${responseData.category} • ID: ${responseData.id}`,
})
await message.reply({
embeds: [embed],
})
} catch {
embed.setDescription('Error occured when fetching meme content.')
await message.reply({
embeds: [embed],
})
}
},
}

View File

@ -0,0 +1,45 @@
import axios from 'axios'
import { EmbedBuilder } from 'discord.js'
import type { TextCommand } from '../../../sturctures/command'
export const command: TextCommand = {
data: {
name: 'meme',
description: "Fetch meme's image.",
directMessageAllowed: true,
cooldownInterval: 6 * 1000,
},
run: async ({ message }) => {
const embed = new EmbedBuilder()
const url = 'https://meme-api.herokuapp.com/gimme'
try {
const response = await axios.get(url)
const responseData: {
url: string
title: string
postLink: string
subreddit: string
} = response.data
embed
.setTitle(responseData.title)
.setImage(responseData.url)
.setFooter({
text: `Credit: ${responseData.subreddit}${responseData.postLink}`,
})
await message.reply({
embeds: [embed],
})
} catch {
embed.setDescription('Error occured when fetching meme content.')
await message.reply({
embeds: [embed],
})
}
},
}

View File

@ -0,0 +1,43 @@
import { EmbedBuilder } from 'discord.js'
import type { TextCommand } from '../../../sturctures/command'
import { callbackEmbed } from '../../../utils/messages'
export const command: TextCommand = {
data: {
name: 'qrcode',
description: 'Generate QR Code by one click.',
directMessageAllowed: true,
cooldownInterval: 5 * 1000,
},
run: async ({ message, args }) => {
if (message.channel.isVoiceBased()) return
const embed = new EmbedBuilder()
const data = args.join(' ')
if (!data) {
const cEmbed = callbackEmbed({
text: 'Missing QR Code data!',
color: 'Red',
mode: 'error',
})
await message.reply({
embeds: [cEmbed],
})
return
}
const url = `https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=${data.replace(
/\s/g,
'%20'
)}`
embed.setImage(url)
await message.channel.send({
embeds: [embed],
})
},
}

View File

@ -0,0 +1,78 @@
import { createCanvas, loadImage } from 'canvas'
import { AttachmentBuilder } from 'discord.js'
import type { TextCommand } from '../../../sturctures/command'
export const command: TextCommand = {
data: {
name: 'rip',
description: 'R I P.',
directMessageAllowed: true,
cooldownInterval: 10 * 1000,
},
run: async ({ message, args }) => {
const { attachments, author, guild, channel } = message
if (channel.isVoiceBased()) return
// Image fetching
let image = attachments.first()?.proxyURL
for (let index = 0; index < 2; index++) {
if (image) break
if (index === 1) {
image = author.displayAvatarURL({
size: 256,
extension: 'png',
forceStatic: true,
})
break
}
if (guild && args[0]) {
if (args[0].length >= 18) {
const idMember = guild.members.cache.get(args[0])
if (idMember) {
image = idMember.user.displayAvatarURL({
size: 256,
extension: 'png',
forceStatic: true,
})
}
} else {
const username = String(args[0]).toLowerCase()
const target = guild.members.cache.find((ur) =>
ur.user.username.toLowerCase().includes(username)
)
if (target) {
image = target.user.displayAvatarURL({
size: 256,
extension: 'png',
forceStatic: true,
})
}
}
}
}
if (!image) return
const targetImage = await loadImage(image)
const background = await loadImage('./assets/rip.jpg')
const canvas = createCanvas(background.width, background.height)
const context = canvas.getContext('2d')
context.drawImage(background, 0, 0, canvas.width, canvas.height)
context.drawImage(targetImage, 95, 200, 150, 150)
const attachment = new AttachmentBuilder(canvas.toBuffer(), {
name: `${Date.now()}_rip.png`,
})
await channel.send({
files: [attachment],
})
},
}

View File

@ -0,0 +1,81 @@
import { createCanvas, loadImage } from 'canvas'
import { AttachmentBuilder } from 'discord.js'
import type { TextCommand } from '../../../sturctures/command'
import { blur } from '../../../utils/canvas'
export const command: TextCommand = {
data: {
name: 'trash',
description: 'TRAshh.',
directMessageAllowed: true,
cooldownInterval: 10 * 1000,
},
run: async ({ message, args }) => {
const { attachments, author, guild, channel } = message
if (channel.isVoiceBased()) return
// Image fetching
let image = attachments.first()?.proxyURL
for (let index = 0; index < 2; index++) {
if (image) break
if (index === 1) {
image = author.displayAvatarURL({
size: 256,
extension: 'png',
forceStatic: true,
})
break
}
if (guild && args[0]) {
if (args[0].length >= 18) {
const idMember = guild.members.cache.get(args[0])
if (idMember) {
image = idMember.user.displayAvatarURL({
size: 256,
extension: 'png',
forceStatic: true,
})
}
} else {
const username = String(args[0]).toLowerCase()
const target = guild.members.cache.find((ur) =>
ur.user.username.toLowerCase().includes(username)
)
if (target) {
image = target.user.displayAvatarURL({
size: 256,
extension: 'png',
forceStatic: true,
})
}
}
}
}
if (!image) return
const blurredImg = await blur(image)
const targetImage = await loadImage(blurredImg)
const background = await loadImage('./assets/trash.png')
const canvas = createCanvas(background.width, background.height)
const context = canvas.getContext('2d')
context.drawImage(background, 0, 0)
context.drawImage(targetImage, 309, 0, 309, 309)
const attachment = new AttachmentBuilder(canvas.toBuffer(), {
name: `${Date.now()}_trash.png`,
})
await channel.send({
files: [attachment],
})
},
}

View File

@ -0,0 +1,108 @@
import { createCanvas, loadImage } from 'canvas'
import { AttachmentBuilder } from 'discord.js'
import GIFEncoder from 'gifencoder'
import type { TextCommand } from '../../../sturctures/command'
export const command: TextCommand = {
data: {
name: 'trigger',
description: 'TRIGGER~.',
directMessageAllowed: true,
cooldownInterval: 10 * 1000,
},
run: async ({ message, args }) => {
const { attachments, author, guild, channel } = message
if (channel.isVoiceBased()) return
// Image fetching
let image = attachments.first()?.proxyURL
for (let index = 0; index < 2; index++) {
if (image) break
if (index === 1) {
image = author.displayAvatarURL({
size: 256,
extension: 'png',
forceStatic: true,
})
break
}
if (guild && args[0]) {
if (args[0].length >= 18) {
const idMember = guild.members.cache.get(args[0])
if (idMember) {
image = idMember.user.displayAvatarURL({
size: 256,
extension: 'png',
forceStatic: true,
})
}
} else {
const username = String(args[0]).toLowerCase()
const target = guild.members.cache.find((ur) =>
ur.user.username.toLowerCase().includes(username)
)
if (target) {
image = target.user.displayAvatarURL({
size: 256,
extension: 'png',
forceStatic: true,
})
}
}
}
}
if (!image) return
const targetImage = await loadImage(image)
const background = await loadImage('./assets/triggered.png')
const gif: any = new GIFEncoder(256, 310)
gif.start()
gif.setRepeat(0)
gif.setDelay(15)
const canvas = createCanvas(256, 310)
const context = canvas.getContext('2d')
const BR = 30
const LR = 20
for (let index = 0; index < 9; index++) {
context.clearRect(0, 0, 256, 310)
context.drawImage(
targetImage,
Math.floor(Math.random() * BR) - BR,
Math.floor(Math.random() * BR) - BR,
256 + BR,
310 - 54 + BR
)
context.fillStyle = '#FF000033'
context.fillRect(0, 0, 256, 310)
context.drawImage(
background,
Math.floor(Math.random() * LR) - LR,
310 - 54 + Math.floor(Math.random() * LR) - LR,
256 + LR,
54 + LR
)
gif.addFrame(context)
}
gif.finish()
const attachment = new AttachmentBuilder(gif.out.getData(), {
name: `${Date.now()}_trgigered.gif`,
})
await channel.send({
files: [attachment],
})
},
}

View File

@ -0,0 +1,57 @@
import { inspect } from 'node:util'
import type { TextCommand } from '../../../sturctures/command'
import { isDev } from '../../../utils/constants'
function clean(text: string) {
if (typeof text === 'string') {
return text
.replaceAll('`', `\`${String.fromCodePoint(8203)}`)
.replaceAll(/@/g, `@${String.fromCodePoint(8203)}`)
}
return text
}
export const command: TextCommand = {
enabled: isDev,
data: {
name: 'eval',
publicLevel: 'None',
description: 'Eval javascript code in Nodejs runtime.',
ownerOnly: true,
developmentOnly: true,
directMessageAllowed: true,
requiredArgs: [
{
type: 'STRING',
rest: true,
},
],
},
run: async ({ message, args }) => {
const { channel } = message
if (channel.isVoiceBased()) return
const code = args.join(' ')
if (code) {
try {
let evaled = eval(code)
if (typeof evaled !== 'string') evaled = inspect(evaled)
if (evaled.length > 1999) return console.log(evaled)
await channel.send({
content: `\`\`\`${clean(evaled as string)}\`\`\``,
})
} catch (error) {
if (error instanceof Error && error.message.length < 1999) {
await channel.send({
content: `\`ERROR\` \`\`\`xl\n${clean(
error.message
)}\n\`\`\``,
})
}
}
}
},
}

View File

@ -0,0 +1,59 @@
import { exec } from 'node:child_process'
import type { TextCommand } from '../../../sturctures/command'
import { isDev } from '../../../utils/constants'
function clean(text: string) {
if (typeof text === 'string') {
return text
.replaceAll('`', `\`${String.fromCodePoint(8203)}`)
.replaceAll(/@/g, `@${String.fromCodePoint(8203)}`)
}
return text
}
export const command: TextCommand = {
enabled: isDev,
data: {
name: 'exec',
publicLevel: 'None',
ownerOnly: true,
developmentOnly: true,
description: 'Execute shell command in Nodejs runtime.',
directMessageAllowed: true,
requiredArgs: [
{
type: 'STRING',
rest: true,
},
],
},
run: ({ message, args }) => {
const { channel } = message
if (channel.isVoiceBased()) return
const code = args.join(' ')
if (code) {
exec(code, async (error, stdout, stderr) => {
if (error) {
await channel.send({
content: `\`ERROR\` \`\`\`xl\n${clean(
error.message
)}\n\`\`\``,
})
return
}
if (stderr) {
await channel.send({
content: `\`ERROR\` \`\`\`xl\n${clean(stderr)}\n\`\`\``,
})
return
}
await channel.send({ content: `\`\`\`${clean(stdout)}\`\`\`` })
})
}
},
}

View File

@ -0,0 +1,73 @@
import AES from 'crypto-js/aes'
import { EmbedBuilder } from 'discord.js'
import type { TextCommand } from '../../../sturctures/command'
export const command: TextCommand = {
data: {
name: 'aes',
description: 'Encrypt and Decrypt stuff by AES256.',
directMessageAllowed: true,
requiredArgs: [
{
name: 'mode',
type: 'STRING',
text: ['encrypt', 'decrypt'],
},
{
name: 'key',
type: 'STRING',
},
{
name: 'value',
type: 'STRING',
},
],
},
run: async ({ message, args }) => {
if (message.channel.isVoiceBased()) return
const [mode, key, ...value] = args
const string = value.join(' ')
const embed = new EmbedBuilder()
if (message.deletable) message.delete().catch(() => undefined)
switch (mode) {
case 'encrypt': {
const encryptedValue = AES.encrypt(string, key).toString()
embed
.setTitle('Encryped Data:')
.setDescription(`\`\`\`${encryptedValue}\`\`\``)
await message.channel.send({
embeds: [embed],
})
break
}
case 'decrypt': {
const decryptedValue = AES.decrypt(string, key).toString()
embed
.setTitle('Decryped Data:')
.setDescription(`\`\`\`${decryptedValue}\`\`\``)
await message.channel.send({
embeds: [embed],
})
break
}
default: {
embed
.setTitle('Wrong Type!')
.setDescription('`encrypt / decrypt` [key] [value...]')
await message.reply({
embeds: [embed],
})
break
}
}
},
}

View File

@ -0,0 +1,53 @@
import { EmbedBuilder } from 'discord.js'
import type { TextCommand } from '../../../sturctures/command'
export const command: TextCommand = {
data: {
name: 'avatar',
aliases: ['av'],
description: "Fetch user's avatar.",
directMessageAllowed: true,
},
run: async ({ message, args }) => {
const embed = new EmbedBuilder()
const { guild, mentions, author } = message
let targetUser = mentions.users.first()
if (!targetUser) {
if (guild && args[0]) {
if (args[0].length >= 18) {
const idMember = guild.members.cache.get(args[0])
if (idMember) {
targetUser = idMember.user
}
} else {
const username = String(args[0]).toLowerCase()
const target = guild.members.cache.find((ur) =>
ur.user.username.toLowerCase().includes(username)
)
if (target) targetUser = target.user
}
} else {
targetUser = author
}
}
if (!targetUser) targetUser = author
const avatarURL = targetUser.displayAvatarURL({
size: 4096,
})
embed
.setTitle(`Icon from ${targetUser.tag}`)
.setDescription(`Link: [Click Here](${avatarURL})`)
.setImage(avatarURL)
await message.reply({
embeds: [embed],
})
},
}

View File

@ -0,0 +1,43 @@
import axios from 'axios'
import { EmbedBuilder } from 'discord.js'
import type { TextCommand } from '../../../sturctures/command'
export const command: TextCommand = {
data: {
name: 'shortenurl',
description: "Fetch meme's image.",
directMessageAllowed: true,
cooldownInterval: 6 * 1000,
},
run: async ({ message, args }) => {
const embed = new EmbedBuilder()
const destination = args[0]
try {
const url = `https://is.gd/create.php?format=simple&url=${encodeURI(
destination
)}`
const response = await axios.get(url)
const responseData: string = response.data
embed
.setTitle('Converted!')
.setDescription(
`URL: ${responseData}\nDestination: \`${destination}\``
)
await message.reply({
embeds: [embed],
})
} catch {
embed.setDescription('Error occured when fetching the content.')
await message.reply({
embeds: [embed],
})
}
},
}

View File

@ -0,0 +1,143 @@
import axios from 'axios'
import { EmbedBuilder } from 'discord.js'
import { parseStringPromise } from 'xml2js'
import type { TextCommand } from '../../../sturctures/command'
export const command: TextCommand = {
data: {
name: 'weather',
description: 'Get weather.',
directMessageAllowed: true,
cooldownInterval: 10 * 1000,
},
run: async ({ message, args }) => {
const embed = new EmbedBuilder()
const targetLocation = args.join(' ')
if (!targetLocation) {
return
}
try {
const url = 'https://weather.service.msn.com/find.aspx'
const response = await axios.get(url, {
params: {
src: 'outlook',
weadegreetype: 'C',
culture: 'en-US',
weasearchstr: targetLocation,
},
timeout: 10_000,
})
const responseData = response.data as string
if (!responseData.includes('<')) {
if (responseData.search(/not found/i) !== -1) {
throw new Error('Location not found!')
}
throw new Error('Unknown error!')
}
const data = await parseStringPromise(responseData)
interface CurrentWeather {
$: {
temperature: string
skytext: string
date: string
observationtime: string
observationpoint: string
feelslike: string
humidity: string
winddisplay: string
day: string
shortday: string
windspeed: string
}
}
interface WeatherData {
$: {
weatherlocationname: string
timezone: string
url: string
imagerelativeurl: string
degreetype: string
entityid: string
}
current: CurrentWeather[]
}
const weatherData: WeatherData = data.weatherdata.weather[0]
embed
.setTitle(
`${weatherData.$.weatherlocationname}'s Current Weather:`
)
.setDescription(
`More Information: [HERE](${weatherData.$.url})`
)
.setFooter({
text: `ID: ${weatherData.$.entityid}`,
})
.addFields([
{
name: 'Date',
value: `${weatherData.current[0].$.date} (${weatherData.current[0].$.day})`,
inline: false,
},
{
name: 'Time Zone',
value: `UTC${
weatherData.$.timezone.startsWith('-')
? weatherData.$.timezone
: '+' + weatherData.$.timezone
}`,
inline: true,
},
{
name: 'Status',
value: weatherData.current[0].$.skytext,
inline: true,
},
{
name: 'Temperature',
value: weatherData.current[0].$.temperature + '°C',
inline: true,
},
{
name: 'Feels like',
value: weatherData.current[0].$.feelslike + '°C',
inline: true,
},
{
name: 'Humidity',
value: weatherData.current[0].$.humidity,
inline: true,
},
{
name: 'Windspeed',
value: weatherData.current[0].$.winddisplay,
inline: true,
},
])
await message.reply({
embeds: [embed],
})
} catch (error) {
if (error instanceof Error) {
embed.setTitle(error.message)
await message.reply({
embeds: [embed],
})
}
}
},
}

View File

@ -0,0 +1,90 @@
import { SlashCommandBuilder } from '@discordjs/builders'
import type { TextChannel } from 'discord.js'
import { EmbedBuilder } from 'discord.js'
import { githubLink, name as botname } from '../../../config/bot.json'
import type { SlashCommand, TextCommand } from '../../sturctures/command'
import { getCommandHelpInfo } from '../../utils/cmds'
import { callbackEmbed } from '../../utils/messages'
import { command as helpTextCommand } from '../message/general/help'
export const command: SlashCommand = {
slashData: new SlashCommandBuilder()
.setName('help')
.setDescription(helpTextCommand.data.description)
.addStringOption((option) =>
option
.setName('command')
.setDescription('Get specified Text Command')
.setRequired(false)
),
run: async ({ interaction }) => {
const embed = new EmbedBuilder()
const { client, channel, options } = interaction
const commandInput = options.get('command')?.value
if (!commandInput || typeof commandInput !== 'string') {
const commandsCatagories = client.commandsCatagories
embed.setDescription(
`Hello🙋!\nOur source code: [Here](${githubLink})\nTurely appreciate that you are supporting us.`
)
for (const catagory of commandsCatagories) {
if (catagory[0].toLocaleLowerCase() === 'nsfw') {
if ((channel as TextChannel).nsfw) {
catagory[0] += ' THIS CHANNEL ONLY'
} else {
continue
}
}
const text = catagory[1]
.map((index: string) => `\`${index}\``)
.join(', ')
embed.addFields([{ name: catagory[0], value: text }])
}
const avatarURL = client.user.defaultAvatarURL
embed.setTitle('Bot Assistance Centre').setFooter({
text: `© ${new Date().getFullYear()} ${botname}`,
iconURL: avatarURL,
})
await interaction.reply({
embeds: [embed],
})
return
}
let cmd: TextCommand | undefined
const commandMatching = client.commands.get(commandInput)
const aliasesMatching = client.aliases.get(commandInput)
// Fetch command destination.
if (commandMatching) {
cmd = commandMatching
} else if (aliasesMatching) {
cmd = client.commands.get(aliasesMatching)
} else {
const cEmbed = callbackEmbed({
text: 'Command requested does not exist!',
color: 'Red',
mode: 'error',
})
await interaction.reply({
embeds: [cEmbed],
})
return
}
if (cmd) {
const helpInfo = getCommandHelpInfo(cmd)
await interaction.reply({
embeds: [helpInfo],
})
}
},
}

View File

@ -0,0 +1,23 @@
import { SlashCommandBuilder } from '@discordjs/builders'
import { EmbedBuilder } from 'discord.js'
import type { SlashCommand } from '../../sturctures/command'
import { command as TextCommand } from '../message/general/ping'
export const command: SlashCommand = {
slashData: new SlashCommandBuilder()
.setName('ping')
.setDescription(TextCommand.data.description),
run: async ({ interaction }) => {
const apiDelayMS = Math.round(interaction.client.ws.ping)
const messageDelayMS = Date.now() - interaction.createdTimestamp
const embed = new EmbedBuilder().setDescription(
`Action Delay: \`${messageDelayMS}ms\`\nAPI Delay: \`${apiDelayMS}ms\``
)
await interaction.reply({
embeds: [embed],
})
},
}

View File

@ -0,0 +1,79 @@
import type { CommandInteraction } from 'discord.js'
import { ownerId } from '../../config/bot.json'
import type { DiscordEvent } from '../sturctures/event'
import { cooldownCache } from '../utils/cache'
import { isDev } from '../utils/constants'
import { parseMsToVisibleText } from '../utils/formatters'
export const event: DiscordEvent = {
name: 'interactionCreate',
// eslint-disable-next-line
run: async (interaction: CommandInteraction) => {
if (interaction.guild && !interaction.member) {
await interaction.guild.members.fetch(interaction.user.id)
}
const returnOfInter = async (content: string, ephemeral = true) => {
await interaction.reply({ content, ephemeral })
}
if (interaction.isChatInputCommand()) {
const { commandName, user, client } = interaction
const slashCollection = client.slashcommands
const slash = slashCollection.get(commandName)
if (!slashCollection.has(commandName) || !slash) {
return returnOfInter('Error Occured!')
}
// Eligibility Validations
if (slash.enabled === false) {
return returnOfInter('This command is not enabled to execute.')
}
if (slash.data?.ownerOnly === true && user.id !== ownerId) {
return returnOfInter('This command is not enabled to execute.')
}
if (slash.data?.developmentOnly === true && !isDev) {
return returnOfInter(
'This command is not enabled to execute in current state.'
)
}
// Cooldown Validation
const now = Date.now()
const keyName = `CMD_${user.id}_${slash.slashData.name}`
const cooldownInterval = slash.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
)
return returnOfInter(
`Before using this command, please wait for **${timeleft}**.`
)
}
}
// Set cooldown
cooldownCache.set(
keyName,
now + cooldownInterval,
cooldownInterval / 1000
)
try {
return slash.run({ interaction })
} catch (error) {
if (error instanceof Error) console.error(error)
}
}
},
}

View File

@ -0,0 +1,456 @@
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 '../../config/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)
}
}
},
}

View File

@ -0,0 +1,26 @@
import type { Client } from 'discord.js'
import { ActivityType } from 'discord.js'
import { defaultPrefix } from '../../config/bot.json'
import type { DiscordEvent } from '../sturctures/event'
export const event: DiscordEvent = {
once: true,
name: 'ready',
run: (client: Client) => {
// Dynamic Status
let index = 0
const status = [`${client.guilds.cache.size} Servers`]
setInterval(() => {
const _status = status[index++ % status.length]
const text = `${defaultPrefix}help | ${_status}`
client.user?.setActivity(text, {
type: ActivityType.Watching,
})
}, 15 * 1000)
console.log('Bot is in ready status!')
},
}

34
packages/bot/src/index.ts Normal file
View File

@ -0,0 +1,34 @@
import 'dotenv/config'
import chalk from 'chalk'
import './validate-env'
import { isDev } from './utils/constants'
// Check NODE Version
const nodeVersions = process.versions.node.split('.')
if (Number(nodeVersions[0]) <= 16 && Number(nodeVersions[1]) < 9) {
throw new Error(
'Node.js version must be 16.9.0 higher. Please update your Node.js version.'
)
}
process.setMaxListeners(15)
// If instacne is not production mode.
if (isDev) {
const log = console.log
log(chalk.bold.red('DEVELOPMENT / MAINTAINANCE MODE'))
log(
chalk.red.bold(
'Some production features will be disrupted or terminated.'
)
)
} else {
process.on('uncaughtException', console.error)
process.on('unhandledRejection', console.error)
}
import('./bot')

View File

@ -0,0 +1,167 @@
import { basename, dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { REST } from '@discordjs/rest'
import chalk from 'chalk'
import type { Client } from 'discord.js'
import type {
RESTGetAPIApplicationCommandsResult,
RESTPostAPIApplicationCommandsJSONBody,
} from 'discord-api-types/v9'
import { Routes } from 'discord-api-types/v9'
import { glob } from 'glob'
import { disabledCommandCatagories } from '../../config/bot.json'
import type { SlashCommand, TextCommand } from '../sturctures/command'
import { isDev } from '../utils/constants'
type TextCommandCatagories = Record<string, string[]>
const __dirname = dirname(fileURLToPath(import.meta.url))
/** Text Command Loaders */
export async function loadTextCommand(client: Client) {
let folderPath = join(__dirname, '../commands/message/**/*.ts')
// Parse path in windows
if (process.platform === 'win32') {
folderPath = folderPath.replaceAll('\\', '/')
}
const allFiles = await glob(folderPath)
if (allFiles.length === 0) {
return
}
const catagories: TextCommandCatagories = {}
for (const filePath of allFiles) {
const commandFile = await import(filePath)
const command: TextCommand = commandFile.command
// Neglect if disabled.
if (command.enabled === false) continue
// Store command to memory.
const cmdName = command.data.name
if (client.commands.has(cmdName)) {
throw new Error('Duplicated command is found!')
}
const catagory = basename(dirname(filePath))
const disabledCatagories: string[] = disabledCommandCatagories
if (!disabledCatagories.includes(catagory)) {
if (catagory) {
command.data.catagory = catagory
if (command.data.publicLevel !== 'None') {
if (!catagories[String(catagory)]) {
catagories[String(catagory)] = []
}
catagories[String(catagory)].push(cmdName)
}
}
if (command.data.intervalLimit) {
const list = command.data.intervalLimit
if (list.minute! > list.hour! || list.hour! > list.day!) {
throw 'Impolitic Custom Interval style!'
}
}
client.commands.set(cmdName, command)
if (command.data.aliases) {
for (const alias of command.data.aliases) {
if (client.aliases.has(alias)) {
throw new Error('Duplicated alias is found!')
}
// Store aliase(s) to memory if exists.
client.aliases.set(alias, command.data.name)
}
}
}
}
for (const value of Object.entries(catagories)) {
client.commandsCatagories.set(value[0], value[1])
}
// Print number of loaded commands.
console.log(
chalk.greenBright.bold(`Loaded ${client.commands.size} text commands.`)
)
}
/** Load Slash commands to API & Collection */
export async function loadSlashCommand(
client: Client,
clientId: string,
token: string
) {
let folderPath = join(__dirname, '../commands/slash/**/*.ts')
// Parse path in windows
if (process.platform === 'win32') {
folderPath = folderPath.replaceAll('\\', '/')
}
const allFiles = await glob(folderPath)
const slashCommandData: RESTPostAPIApplicationCommandsJSONBody[] = []
for (const filePath of allFiles) {
const commandFile = await import(filePath)
const slashCommand: SlashCommand = commandFile.command
const slashCommandCollection = client.slashcommands
const name = slashCommand.slashData.name
if (slashCommandCollection.has(name)) {
throw new Error('Duplicated slash command is found!')
}
client.slashcommands.set(name, slashCommand)
slashCommandData.push(slashCommand.slashData.toJSON())
}
const rest = new REST({ version: '9' }).setToken(token)
if (isDev) {
// Guild & Development Commands.
const guildId = process.env.DEV_GUILD_ID
if (guildId) {
const guildCommands = await rest.get(
Routes.applicationGuildCommands(clientId, guildId)
)
for (const command of guildCommands as RESTGetAPIApplicationCommandsResult) {
const deleteUrl = `${Routes.applicationGuildCommands(
clientId,
guildId
)}/${command.id}`
await rest.delete(`/${deleteUrl}`)
}
await rest.put(Routes.applicationGuildCommands(clientId, guildId), {
body: slashCommandData,
})
}
} else {
// Global Commands
await rest.put(Routes.applicationCommands(clientId), {
body: slashCommandData,
})
}
// Print number of loaded commands.
console.log(
chalk.greenBright.bold(
`Loaded ${client.slashcommands.size} slash commands.`
)
)
}

View File

@ -0,0 +1,45 @@
import { dirname, join } from 'node:path'
import { fileURLToPath } from 'node:url'
import chalk from 'chalk'
import type { Client } from 'discord.js'
import { glob } from 'glob'
import type { DiscordEvent } from '../sturctures/event'
const __dirname = dirname(fileURLToPath(import.meta.url))
export async function loadDiscordEvent(client: Client) {
let folderPath = join(__dirname, '../events/**/*.ts')
// Parse path in windows
if (process.platform === 'win32') {
folderPath = folderPath.replaceAll('\\', '/')
}
const allFiles = await glob(folderPath)
if (allFiles.length === 0) {
return
}
for (const filePath of allFiles) {
// Get event content.
const eventFile = await import(filePath)
const event: DiscordEvent = eventFile.event
// Check triggering mode.
if (event.once === true) {
// eslint-disable-next-line
client.once(event.name, event.run.bind(undefined))
} else {
// eslint-disable-next-line
client.on(event.name, event.run.bind(undefined))
}
}
// Print number of loaded events.
console.log(
chalk.greenBright.bold(`Loaded ${allFiles.length} Discord events.`)
)
}

View File

@ -0,0 +1,93 @@
import type { SlashCommandBuilder } from '@discordjs/builders'
import type {
CommandInteraction,
Message,
PermissionResolvable,
} from 'discord.js'
interface TextCommandExecution {
message: Message
args: string[]
}
interface SlashCommandExecution {
interaction: CommandInteraction
}
interface TextCommandRequiredArgumentsDefault {
name?: string
rest?: boolean
text?: string[]
type: 'NUMBER' | 'STRING'
customLength?: {
min?: number
max?: number
}
required?: boolean
}
type TextCommandRequiredArguments = TextCommandRequiredArgumentsDefault
/**
* As default, command can only be accessed in guild.
*
* Everyone can access wihtout any permission limitations.
*
* Cooldown Interval is 3 seconds (3000 milliseconds)
*/
export interface TextCommand {
enabled?: boolean
// Command Data
readonly data: {
// Permissions
clientRequiredPermissions?: PermissionResolvable[]
authorRequiredPermissions?: PermissionResolvable[]
// Access, Environment & Scenes
ownerOnly?: boolean
developmentOnly?: boolean
nsfwChannelRequired?: boolean
inVoiceChannelRequired?: boolean
threadChannelAllowed?: boolean
directMessageAllowed?: boolean
publicLevel?: 'All' | 'Permission' | 'None'
requiredArgs?: TextCommandRequiredArguments[]
// Info
name: string
description: string
catagory?: string
usage?: string
aliases?: string[]
// Specified Configurations
cooldownInterval?: number
intervalLimit?: {
minute?: number
hour?: number
day?: number
}
}
// eslint-disable-next-line no-unused-vars
run: ({ message, args }: TextCommandExecution) => Promise<void> | void
}
export interface SlashCommand {
enabled?: boolean
// Slash Data
slashData: Omit<SlashCommandBuilder, 'addSubcommand' | 'addSubcommandGroup'>
readonly data?: {
clientRequiredPermissions?: PermissionResolvable[]
ownerOnly?: boolean
developmentOnly?: boolean
cooldownInterval?: number
}
// eslint-disable-next-line no-unused-vars
run: ({ interaction }: SlashCommandExecution) => Promise<void>
}

View File

@ -0,0 +1,67 @@
export interface GuildConfig {
prefix: string
serverId: string
commands: {
global: {
// Access
disabled: string[]
disabledCatagories: string[]
// Cooldown
customCooldown: {
name: string
value: number
intervals: {
minute: number
hour: number
day: number
}
}[]
}
// Access
userDisabled: { id: string; cmds: string[] }[]
roleDisabled: { id: string; cmds: string[] }[]
channelDisabled: { id: string; cmds: string[] }[]
}
antiSpam: {
enabled: boolean
whitelistedUsers: string[]
whitelistedRoles: string[]
whitelistedChannels: string[]
inviteLinks: {
enabled: boolean
whitelistedUsers: string[]
whitelistedRoles: string[]
whitelistedChannels: string[]
}
mentions: {
enabled: boolean
maxmiumCheck: {
enabled: boolean
value: number
}
publicRoleCheck: boolean
whitelistedUsers: string[]
whitelistedRoles: string[]
whitelistedChannels: string[]
}
}
thread: {
listen?: boolean
}
snipe: {
channelDisabled: string[]
}
}
export interface SnipeConfig {
channelId: string
author: {
id: string
name: string
}
content: {
text: string
date: number
imageURL: string | undefined
}
}

View File

@ -0,0 +1,10 @@
import type { ClientEvents } from 'discord.js'
/** Discord Client events */
export interface DiscordEvent {
// Event Data
name: keyof ClientEvents
once?: boolean
// eslint-disable-next-line no-unused-vars
run: (...arguments_: any[]) => Promise<void> | void
}

View File

@ -0,0 +1,5 @@
import NodeCache from 'node-cache'
const cooldownCache = new NodeCache()
export { cooldownCache }

View File

@ -0,0 +1,28 @@
import { createCanvas, loadImage } from 'canvas'
async function blur(image: string | Buffer): Promise<Buffer> {
if (!image) throw new Error('Image was not provided!')
const img = await loadImage(image)
const canvas = createCanvas(img.width, img.height)
const context = canvas.getContext('2d')
context.fillStyle = '#ffffff'
context.fillRect(0, 0, canvas.width, canvas.height)
context.drawImage(img, 0, 0, canvas.width / 4, canvas.height / 4)
context.imageSmoothingEnabled = true
context.drawImage(
canvas,
0,
0,
canvas.width / 4,
canvas.height / 4,
0,
0,
canvas.width + 5,
canvas.height + 5
)
return canvas.toBuffer()
}
export { blur }

View File

@ -0,0 +1,127 @@
import type {
CollectorFilter,
Message,
MessageComponentInteraction,
} from 'discord.js'
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
EmbedBuilder,
} from 'discord.js'
import { findBestMatch } from 'string-similarity'
import type { TextCommand } from '../sturctures/command'
/** Send command information message embed. */
export function getCommandHelpInfo(cmd: TextCommand): EmbedBuilder {
const embed = new EmbedBuilder()
.setTitle(`Command: ${cmd.data.name}`)
.addFields([{ name: 'Description', value: cmd.data.description }])
if (cmd.data.usage) {
embed.addFields([{ name: 'Usage', value: cmd.data.usage }])
}
embed.addFields([
{ name: 'Catagory', value: cmd.data.catagory ?? '', inline: true },
{
name: 'Cooldown',
value: `${
(cmd.data.cooldownInterval ?? 3000) / 1000 || '3'
} seconds`,
inline: true,
},
])
if (cmd.data.intervalLimit) {
embed.addFields([
{
name: 'Allowed Intervals',
value: Object.entries(cmd.data.intervalLimit)
.map((value) => {
return `${value[0]} - \`${value[1]}\``
})
.join('\n'),
inline: false,
},
])
}
return embed
}
interface ExpectedWord {
name: string
timeTaken: number
}
/** Similarity check of excuting commands. */
export async function resembleCommandCheck(
message: Message,
word: string
): Promise<ExpectedWord | undefined> {
const timeStarted = Date.now()
const cmdList = [...message.client.commands.keys()]
const { bestMatch } = findBestMatch(word, cmdList)
if (bestMatch.rating < 0.65) return undefined
const acceptButtonId = 'accCMD'
const declineButtonId = 'denCMD'
const acceptButton = new ButtonBuilder()
.setStyle(ButtonStyle.Success)
.setEmoji('👍')
.setCustomId(acceptButtonId)
const declineButton = new ButtonBuilder()
.setStyle(ButtonStyle.Danger)
.setEmoji('👎')
.setCustomId(declineButtonId)
const row: ActionRowBuilder<any> = new ActionRowBuilder().addComponents([
acceptButton,
declineButton,
])
const matchRate = Number(bestMatch.rating.toFixed(2))
const _message = await message.reply({
content: `Do you prefer excuting \`${
bestMatch.target
}\`? (Similarity: \`${matchRate * 100}%\`)`,
components: [row],
})
const filter: CollectorFilter<[MessageComponentInteraction]> = (inter) => {
return (
[acceptButtonId, declineButtonId].includes(inter.customId) &&
inter.user.id === message.author.id
)
}
return new Promise((resolve) => {
return _message
.awaitMessageComponent({
filter,
time: 30_000,
})
.then(async (_inter) => {
// Delete message first
if (_message.deletable) {
await _message.delete().catch(() => undefined)
}
if (_inter.customId === acceptButtonId) {
const timeTaken = timeStarted - Date.now()
return resolve({ name: bestMatch.target, timeTaken })
}
throw new Error('Declined')
})
.catch(() => {
// eslint-disable-next-line
return resolve(undefined)
})
})
}

View File

@ -0,0 +1 @@
export const isDev = process.env.NODE_ENV === 'development'

View File

@ -0,0 +1,17 @@
import { GraphQLClient } from 'graphql-request'
export let client = new GraphQLClient('')
export const hgqlInit = () => {
console.log('\n🚀 GraphQL Client Initialized')
let HASURA_URL: string = process.env.HASURA_GRAPHQL_ENDPOINT || ''
HASURA_URL += HASURA_URL.endsWith('/') ? 'v1/graphql' : '/v1/graphql'
const HASURA_ADMIN: string = process.env.HASURA_GRAPHQL_ADMIN_SECRET || ''
client = new GraphQLClient(HASURA_URL, {
headers: {
'x-hasura-admin-secret': HASURA_ADMIN,
},
})
}

View File

@ -0,0 +1,16 @@
/**
* Transfer milliseconds to visible duration
*
* 1000 -> 0h 0m 1s
* */
export function parseMsToVisibleText(ms: number): string {
if (ms < 1000) return 'less than 1s'
let seconds: number | string = Math.trunc((ms / 1000) % 60)
let minutes: number | string = Math.trunc((ms / (1000 * 60)) % 60)
let hours: number | string = Math.trunc((ms / (1000 * 60 * 60)) % 24)
seconds = seconds > 0 ? `${seconds}s` : ''
minutes = minutes > 0 ? `${minutes}m ` : ''
hours = hours > 0 ? `${hours}h ` : ''
return `${hours}${minutes}${seconds}`
}

View File

@ -0,0 +1,93 @@
import type {
CollectorFilter,
ColorResolvable,
EmbedField,
Message,
MessageComponentInteraction,
} from 'discord.js'
import {
ActionRowBuilder,
ButtonBuilder,
ButtonStyle,
EmbedBuilder,
} from 'discord.js'
import emoji from '../../config/emojis.json'
interface ConfirmInformationButtons {
title: string
message: Message
fields: EmbedField[]
}
export async function confirmInformationButtons({
title,
fields,
message,
}: ConfirmInformationButtons): Promise<boolean> {
if (message.channel.isVoiceBased()) return false
const now = Date.now()
const embed = new EmbedBuilder().setTitle(title).addFields(fields)
const confirmId = `CONFIRM_${now}`
const cancelId = `CANCEL_${now}`
const buttonSuccess = new ButtonBuilder()
.setStyle(ButtonStyle.Success)
.setLabel('Confirm')
.setCustomId(confirmId)
const buttonCancel = new ButtonBuilder()
.setStyle(ButtonStyle.Danger)
.setLabel('Cancel')
.setCustomId(cancelId)
const row: ActionRowBuilder<any> = new ActionRowBuilder().addComponents([
buttonSuccess,
buttonCancel,
])
const respondAwaiting = await message.channel.send({
embeds: [embed],
components: [row],
})
const filter: CollectorFilter<[MessageComponentInteraction]> = (inter) => {
return (
[confirmId, cancelId].includes(inter.customId) &&
inter.user.id === message.author.id
)
}
const interaction = await respondAwaiting.awaitMessageComponent({
filter,
time: 30_000,
})
if (respondAwaiting.deletable)
await respondAwaiting.delete().catch(() => undefined)
return interaction.customId === confirmId
}
interface CallbackEmbed {
text: string
color?: ColorResolvable
mode?: 'error' | 'success' | 'warning'
}
export function callbackEmbed({
text,
color = 'Grey',
mode,
}: CallbackEmbed): EmbedBuilder {
let emojiText = ''
if (mode && typeof mode === 'string') {
emojiText = emoji[mode]
}
return new EmbedBuilder()
.setDescription(`${emojiText} ${text}`)
.setColor(color)
}

View File

@ -0,0 +1,22 @@
import { z } from 'zod'
declare global {
// eslint-disable-next-line @typescript-eslint/no-namespace
namespace NodeJS {
// eslint-disable-next-line
interface ProcessEnv extends z.infer<typeof ZodEnvironmentVariables> {}
}
}
const ZodEnvironmentVariables = z.object({
TOKEN: z.string(),
CLIENT_ID: z.string(),
DEV_GUILD_ID: z.string().optional(),
PORT: z.string().optional(),
HASURA_GRAPHQL_ADMIN_SECRET: z.string(),
HASURA_GRAPHQL_ENDPOINT: z.string(),
})
ZodEnvironmentVariables.parse(process.env)
console.log('✅ Environment variables verified!')

View File

@ -0,0 +1,37 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"compilerOptions": {
"allowJs": true,
"allowSyntheticDefaultImports": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"alwaysStrict": true,
"checkJs": true,
"esModuleInterop": true,
"forceConsistentCasingInFileNames": true,
"isolatedModules": true,
"module": "esnext",
"moduleResolution": "node",
"noEmit": true,
"noFallthroughCasesInSwitch": true,
"noImplicitAny": true,
"noImplicitOverride": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noUncheckedIndexedAccess": false,
"noUnusedLocals": true,
"noUnusedParameters": true,
"resolveJsonModule": true,
"skipLibCheck": true,
"strict": true,
"strictFunctionTypes": true,
"strictNullChecks": true,
"strictPropertyInitialization": true,
"target": "esnext"
},
"display": "Default tsconfig.json",
"exclude": ["node_modules"]
}