mirror of
https://github.com/discordeno/discordeno.git
synced 2026-05-21 02:40:08 +00:00
feat(examples): Migrate example bots to discordeno v19 (#3647)
* Migrate beginner and minimal bot to discordeno v19
* Add .swcrc and fix minimal yarn.lock
* update .gitignore files
* Update nodejs template
Discordeno.js (DD v13) -> DD v19 "raw"
Currently the permission checking is not working correctly
* Fix permission issue
* Rename the templates
* remove unused indents
* Rename starter to beginner
So now it is minimal (main branch) -> beginner
* Really small refactor & eslint fixes (bigbot template)
This is to make my life less miserable at a later time
* mark rabbitMQ plugins as binary files
git seems to be treating them as text
* Add v19 bigbot rest
* Add gateway code
and rabbitmq_message_deduplication v0.6.2 plugin
* fix yarn messy semevr version for @types/amqplib
* clear channel con amqp connection close
* Add bot code for bigbot v19
missing prisma setup, collector setup & language setup
* Add localization
The "command versioning" system works the same say as before, but instead of a command version the code updates the commands every time they change based on the SHA1 of the commands
* Use file relative paths instead of cwd relative paths
* Fix todos
* revert autocomplete tests
* Revert "Add localization"
This reverts commit 2b1da8d2cd.
* move env assertion to config.ts
* Add shard ping to /ping
* fix small issue
* Update readme files
* use Date.now() for the bigbot REST ping
* Remove bigbot v16 code
* Add docker (compose) setup to bigbot template
* remove healthchecks from rest & gateway
* Update dependencies of examples
* Apply readme(s) suggestions from code review
Hopefully i haven't missed any related to markdown files
Co-authored-by: LTS20050703 <lts20050703@gmail.com>
* Apply code suggestions from code review
Co-authored-by: LTS20050703 <lts20050703@gmail.com>
---------
Co-authored-by: LTS20050703 <lts20050703@gmail.com>
This commit is contained in:
@@ -1,13 +0,0 @@
|
||||
FROM mcr.microsoft.com/vscode/devcontainers/base:0-buster
|
||||
|
||||
ENV DENO_INSTALL=/deno
|
||||
RUN mkdir -p /deno \
|
||||
&& curl -fsSL https://deno.land/x/install/install.sh | sh \
|
||||
&& chown -R vscode /deno
|
||||
|
||||
ENV PATH=${DENO_INSTALL}/bin:${PATH} \
|
||||
DENO_DIR=${DENO_INSTALL}/.cache/deno
|
||||
|
||||
# [Optional] Uncomment this section to install additional OS packages.
|
||||
# RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \
|
||||
# && apt-get -y install --no-install-recommends <your-package-list-here>
|
||||
@@ -1,51 +0,0 @@
|
||||
// For format details, see https://aka.ms/devcontainer.json. For config options, see the README at:
|
||||
// https://github.com/microsoft/vscode-dev-containers/tree/v0.162.0/containers/deno
|
||||
{
|
||||
"name": "Deno",
|
||||
"dockerFile": "Dockerfile",
|
||||
// Set *default* container specific settings.json values on container create.
|
||||
"settings": {
|
||||
"terminal.integrated.shell.linux": "/bin/bash",
|
||||
"deno.enable": true,
|
||||
"editor.defaultFormatter": "denoland.vscode-deno",
|
||||
"editor.minimap.enabled": false,
|
||||
"editor.wordWrap": "on",
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true,
|
||||
"source.fixAll": true
|
||||
},
|
||||
"editor.fontSize": 16,
|
||||
"workbench.colorTheme": "Material Theme Darker",
|
||||
"workbench.iconTheme": "eq-material-theme-icons-darker",
|
||||
"breadcrumbs.enabled": true,
|
||||
"editor.renderWhitespace": "all",
|
||||
"editor.suggestSelection": "first",
|
||||
"editor.formatOnSave": true,
|
||||
"files.autoSave": "afterDelay",
|
||||
"editor.fontFamily": "Fira Code, Menlo, Monaco, 'Courier New', monospace",
|
||||
"typescript.updateImportsOnFileMove.enabled": "always",
|
||||
"javascript.updateImportsOnFileMove.enabled": "always",
|
||||
"deno.inlayHints.enumMemberValues.enabled": true,
|
||||
"deno.inlayHints.functionLikeReturnTypes.enabled": true,
|
||||
"deno.inlayHints.parameterNames.enabled": "all",
|
||||
"deno.inlayHints.parameterNames.suppressWhenArgumentMatchesName": false,
|
||||
"deno.inlayHints.parameterTypes.enabled": true,
|
||||
"deno.inlayHints.propertyDeclarationTypes.enabled": true,
|
||||
"deno.inlayHints.variableTypes.enabled": true,
|
||||
"deno.inlayHints.variableTypes.suppressWhenTypeMatchesName": false
|
||||
},
|
||||
// Add the Ids of extensions you want installed when the container is created.
|
||||
"extensions": [
|
||||
"denoland.vscode-deno",
|
||||
"pkief.material-icon-theme",
|
||||
"coenraads.bracket-pair-colorizer",
|
||||
"equinusocio.vsc-material-theme",
|
||||
"tabnine.tabnine-vscode"
|
||||
],
|
||||
// Use 'forwardPorts' to make a list of ports inside the container available locally.
|
||||
// "forwardPorts": [],
|
||||
// Uncomment to use the Docker CLI from inside the container. See https://aka.ms/vscode-remote/samples/docker-from-docker.
|
||||
// "mounts": [ "source=/var/run/docker.sock,target=/var/run/docker.sock,type=bind" ],
|
||||
// Comment out connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root.
|
||||
"remoteUser": "vscode"
|
||||
}
|
||||
9
examples/.gitignore
vendored
9
examples/.gitignore
vendored
@@ -1,9 +0,0 @@
|
||||
.env
|
||||
fileloader.ts
|
||||
|
||||
config.json
|
||||
|
||||
# Folder made by Kwik db to watch guild command versions
|
||||
db/
|
||||
node_modules
|
||||
package-lock.json
|
||||
24
examples/.vscode/settings.json
vendored
24
examples/.vscode/settings.json
vendored
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"deno.enable": true,
|
||||
"deno.lint": true,
|
||||
"editor.formatOnSave": true,
|
||||
"editor.codeActionsOnSave": {
|
||||
"source.organizeImports": true,
|
||||
"source.fixAll": true
|
||||
},
|
||||
"editor.defaultFormatter": "denoland.vscode-deno",
|
||||
"deno.suggest.imports.hosts": {
|
||||
"https://deno.land": true
|
||||
},
|
||||
"discord.enabled": true,
|
||||
"cSpell.words": ["denoland", "Discordeno", "Kwik", "Loglevels", "msgpack", "Slowmode", "upsert"],
|
||||
"deno.unstable": true,
|
||||
"deno.inlayHints.enumMemberValues.enabled": true,
|
||||
"deno.inlayHints.functionLikeReturnTypes.enabled": true,
|
||||
"deno.inlayHints.parameterNames.enabled": "all",
|
||||
"deno.inlayHints.parameterNames.suppressWhenArgumentMatchesName": false,
|
||||
"deno.inlayHints.parameterTypes.enabled": true,
|
||||
"deno.inlayHints.propertyDeclarationTypes.enabled": true,
|
||||
"deno.inlayHints.variableTypes.enabled": true,
|
||||
"deno.inlayHints.variableTypes.suppressWhenTypeMatchesName": false
|
||||
}
|
||||
@@ -1,10 +1,27 @@
|
||||
# Discordeno Bot Template
|
||||
# Discordeno Bot Templates
|
||||
|
||||
In this directory you will find some example bots written using Discordeno.
|
||||
|
||||
In each template directory you will find more information on how to setup and run the template.
|
||||
|
||||
## Minimal & Beginner
|
||||
|
||||
A very minimal bot with only a /ping command to show how to set-up a discordeno bot for interactions
|
||||
|
||||
The beginner template is a bit more complete and has a caching system already setup using the `dd-cache-proxy` library
|
||||
|
||||
## Advanced
|
||||
|
||||
A more complex bot compared to beginner. It also has a /ping command, but also a /warn command showing how to deal with permissions and sending DMs
|
||||
|
||||
This template has a caching system already setup using the `dd-cache-proxy` library
|
||||
|
||||
## BigBot
|
||||
|
||||

|
||||
|
||||
This repo is meant as a template which you can use to create a Discord bot very easily using the
|
||||
[Discordeno library](https://github.com/discordeno/discordeno).
|
||||
The BigBot template is intended for more complex systems that need scaling.
|
||||
|
||||
[Website/Guide](https://discordeno.js.org/)
|
||||
The template consists of 3 folders with some common files. The template is configured with a REST proxy, the Gateway in a separate process and the Bot code in another.
|
||||
|
||||
[Discord Server](https://discord.com/invite/5vBgXk3UcZ)
|
||||
While the template does not include any caching by default, you can either install `dd-cache-proxy` and setup it or roll your own solution
|
||||
|
||||
1
examples/advanced/.env.example
Normal file
1
examples/advanced/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
TOKEN=
|
||||
32
examples/advanced/.gitignore
vendored
Normal file
32
examples/advanced/.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# build
|
||||
dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
24
examples/advanced/.swcrc
Normal file
24
examples/advanced/.swcrc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"decorators": true,
|
||||
"dynamicImport": true
|
||||
},
|
||||
"transform": {
|
||||
"legacyDecorator": true,
|
||||
"decoratorMetadata": true
|
||||
},
|
||||
"target": "es2022",
|
||||
"keepClassNames": true,
|
||||
"loose": true
|
||||
},
|
||||
"module": {
|
||||
"type": "es6",
|
||||
"strict": false,
|
||||
"strictMode": true,
|
||||
"lazy": false,
|
||||
"noInterop": false
|
||||
}
|
||||
}
|
||||
20
examples/advanced/README.md
Normal file
20
examples/advanced/README.md
Normal file
@@ -0,0 +1,20 @@
|
||||
# Advanced Bot Template
|
||||
|
||||
This template contains more advanced code.
|
||||
|
||||
This template includes caching (using `dd-proxy-cache`), user permission handling, and slash commands options support
|
||||
This template also includes a /ping command to show the bot latency and a /warn command to show how to send a DM to a user and how to check for permissions
|
||||
|
||||
## Setup
|
||||
|
||||
- Download the source
|
||||
- Install the dependencies using `yarn`
|
||||
- Copy the .env.example file and rename it to .env
|
||||
- Fill out the .env file
|
||||
|
||||
## Run Bot
|
||||
|
||||
- run `yarn` to install the dependencies
|
||||
- run `yarn build` to build the source
|
||||
- run `node dist/register-commands.js` to register the slash commands
|
||||
- run `yarn start` to run the bot
|
||||
26
examples/advanced/package.json
Normal file
26
examples/advanced/package.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"name": "dd-advanced-bot",
|
||||
"version": "1.0.0",
|
||||
"description": "A bit more advanced bot that has some permission handling and slash command options.",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"license": "ISC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.0.2",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"build": "swc src --strip-leading-paths --delete-dir-on-start --out-dir dist",
|
||||
"setup-dd": ""
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordeno/bot": "19.0.0-next.92bf166",
|
||||
"dd-cache-proxy": "^2.1.1",
|
||||
"dotenv": "^16.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.3.12",
|
||||
"@swc/core": "^1.6.3",
|
||||
"@types/node": "^20.14.6",
|
||||
"typescript": "^5.5.2"
|
||||
}
|
||||
}
|
||||
49
examples/advanced/src/bot.ts
Normal file
49
examples/advanced/src/bot.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
import { createBot, Intents, LogDepth, type logger as discordenoLogger } from '@discordeno/bot'
|
||||
import { createProxyCache } from 'dd-cache-proxy'
|
||||
import { configs } from './config.js'
|
||||
|
||||
export const bot = createProxyCache(
|
||||
createBot({
|
||||
token: configs.token,
|
||||
intents: Intents.Guilds,
|
||||
}),
|
||||
{
|
||||
desiredProps: {
|
||||
guilds: ['id', 'name', 'roles'],
|
||||
roles: ['id', 'guildId', 'permissions'],
|
||||
},
|
||||
cacheInMemory: {
|
||||
guilds: true,
|
||||
roles: true,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// By default, bot.logger will use an instance of the logger from @discordeno/bot, this logger supports depth and we need to change it, so we need to say to TS that we know what we are doing with as
|
||||
;(bot.logger as typeof discordenoLogger).setDepth(LogDepth.Full)
|
||||
|
||||
// Setup desired proprieties
|
||||
bot.transformers.desiredProperties.interaction.id = true
|
||||
bot.transformers.desiredProperties.interaction.type = true
|
||||
bot.transformers.desiredProperties.interaction.data = true
|
||||
bot.transformers.desiredProperties.interaction.token = true
|
||||
bot.transformers.desiredProperties.interaction.guildId = true
|
||||
bot.transformers.desiredProperties.interaction.member = true
|
||||
|
||||
bot.transformers.desiredProperties.guild.id = true
|
||||
bot.transformers.desiredProperties.guild.name = true
|
||||
bot.transformers.desiredProperties.guild.roles = true
|
||||
|
||||
bot.transformers.desiredProperties.role.id = true
|
||||
bot.transformers.desiredProperties.role.guildId = true
|
||||
bot.transformers.desiredProperties.role.permissions = true
|
||||
|
||||
bot.transformers.desiredProperties.member.id = true
|
||||
bot.transformers.desiredProperties.member.roles = true
|
||||
|
||||
bot.transformers.desiredProperties.channel.id = true
|
||||
|
||||
bot.transformers.desiredProperties.user.id = true
|
||||
bot.transformers.desiredProperties.user.username = true
|
||||
bot.transformers.desiredProperties.user.discriminator = true
|
||||
20
examples/advanced/src/commands.ts
Normal file
20
examples/advanced/src/commands.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { Collection, type ApplicationCommandOption, type ApplicationCommandTypes, type Interaction } from '@discordeno/bot'
|
||||
|
||||
export const commands = new Collection<string, Command>()
|
||||
|
||||
export function createCommand(command: Command): void {
|
||||
commands.set(command.name, command)
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
/** The name of this command. */
|
||||
name: string
|
||||
/** What does this command do? */
|
||||
description: string
|
||||
/** The type of command this is. */
|
||||
type: ApplicationCommandTypes
|
||||
/** The options for this command */
|
||||
options?: ApplicationCommandOption[]
|
||||
/** This will be executed when the command is run. */
|
||||
execute: (interaction: Interaction, options: Record<string, unknown>) => unknown
|
||||
}
|
||||
15
examples/advanced/src/commands/ping.ts
Normal file
15
examples/advanced/src/commands/ping.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { ApplicationCommandTypes, createEmbeds, snowflakeToTimestamp } from '@discordeno/bot'
|
||||
import { createCommand } from '../commands.js'
|
||||
|
||||
createCommand({
|
||||
name: 'ping',
|
||||
description: 'See if the bot latency is okay',
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
async execute(interaction) {
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id)
|
||||
|
||||
const embeds = createEmbeds().setTitle(`The bot ping is ${ping}ms`)
|
||||
|
||||
await interaction.respond({ embeds })
|
||||
},
|
||||
})
|
||||
80
examples/advanced/src/commands/warn.ts
Normal file
80
examples/advanced/src/commands/warn.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
import { ApplicationCommandOptionTypes, ApplicationCommandTypes, createEmbeds, Permissions, type Member, type User } from '@discordeno/bot'
|
||||
import { bot } from '../bot.js'
|
||||
import { createCommand } from '../commands.js'
|
||||
import { calculateMemberPermissions } from '../utils/permissions.js'
|
||||
|
||||
createCommand({
|
||||
name: 'warn',
|
||||
description: 'Warn a user from the server',
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
options: [
|
||||
{
|
||||
name: 'user',
|
||||
description: 'The user you want to warn',
|
||||
type: ApplicationCommandOptionTypes.User,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'reason',
|
||||
description: 'The reason for the warn',
|
||||
type: ApplicationCommandOptionTypes.String,
|
||||
maxLength: 300,
|
||||
},
|
||||
],
|
||||
async execute(interaction, options) {
|
||||
if (!interaction.guildId || !interaction.member) {
|
||||
await interaction.respond('This command can only be ran in guilds')
|
||||
return
|
||||
}
|
||||
|
||||
// Type based on the options declared above
|
||||
const { user, reason } = options as { user: UserResolved; reason?: string }
|
||||
|
||||
const guild = await bot.cache.guilds.get(interaction.guildId)
|
||||
|
||||
if (!guild) {
|
||||
await interaction.respond('An error has occurred')
|
||||
return
|
||||
}
|
||||
|
||||
await interaction.defer()
|
||||
|
||||
const perms = new Permissions(await calculateMemberPermissions(guild, interaction.member))
|
||||
|
||||
const adminPerm = perms.has('ADMINISTRATOR')
|
||||
const kickMembersPerm = adminPerm || perms.has('KICK_MEMBERS')
|
||||
|
||||
if (!kickMembersPerm) {
|
||||
await interaction.respond("You don't have the necessary permissions to warn a members (this command requires `Kick members`)")
|
||||
return
|
||||
}
|
||||
|
||||
const embeds = createEmbeds()
|
||||
.setTitle('Warned User:')
|
||||
.setDescription(`User: <@${user.user.id}>\nReason: ${reason}`)
|
||||
.setColor(0x00ff00)
|
||||
.setTimestamp(Date.now())
|
||||
|
||||
const warnEmbeds = createEmbeds()
|
||||
.setTitle('Warning:')
|
||||
.setDescription(`You have been warned in **${guild.name}** for \`${reason}\``)
|
||||
.setTimestamp(Date.now())
|
||||
|
||||
try {
|
||||
const dmChannel = await bot.helpers.getDmChannel(user.user.id)
|
||||
await bot.helpers.sendMessage(dmChannel.id, { embeds: warnEmbeds })
|
||||
} catch (error) {
|
||||
bot.logger.error(`There was an error in the warn command:`, error)
|
||||
|
||||
await interaction.respond(`Could not warn user <@${user.user.id}> | They likely do not have their DMs open.`)
|
||||
return
|
||||
}
|
||||
|
||||
await interaction.respond({ embeds })
|
||||
},
|
||||
})
|
||||
|
||||
interface UserResolved {
|
||||
user: User
|
||||
member?: Member
|
||||
}
|
||||
11
examples/advanced/src/config.ts
Normal file
11
examples/advanced/src/config.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
const token = process.env.TOKEN
|
||||
|
||||
if (!token) throw new Error('Missing TOKEN environment variable')
|
||||
|
||||
export const configs: Config = {
|
||||
token,
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
token: string
|
||||
}
|
||||
22
examples/advanced/src/events/interactionCreate.ts
Normal file
22
examples/advanced/src/events/interactionCreate.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { InteractionTypes, commandOptionsParser } from '@discordeno/bot'
|
||||
import { bot } from '../bot.js'
|
||||
import { commands } from '../commands.js'
|
||||
|
||||
bot.events.interactionCreate = async (interaction) => {
|
||||
if (!interaction.data || interaction.type !== InteractionTypes.ApplicationCommand) return
|
||||
|
||||
const command = commands.get(interaction.data.name)
|
||||
|
||||
if (!command) {
|
||||
bot.logger.error(`Command ${interaction.data.name} not found`)
|
||||
return
|
||||
}
|
||||
|
||||
const options = commandOptionsParser(interaction)
|
||||
|
||||
try {
|
||||
await command.execute(interaction, options)
|
||||
} catch (error) {
|
||||
bot.logger.error(`There was an error running the ${command.name} command.`, error)
|
||||
}
|
||||
}
|
||||
8
examples/advanced/src/events/ready.ts
Normal file
8
examples/advanced/src/events/ready.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import { bot } from '../bot.js'
|
||||
|
||||
bot.events.ready = ({ user, shardId }) => {
|
||||
if (shardId === bot.gateway.lastShardId) {
|
||||
// All shards are ready
|
||||
bot.logger.info(`Successfully connected to the gateway as ${user.username}#${user.discriminator}`)
|
||||
}
|
||||
}
|
||||
14
examples/advanced/src/index.ts
Normal file
14
examples/advanced/src/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import 'dotenv/config'
|
||||
|
||||
import { bot } from './bot.js'
|
||||
import importDirectory from './utils/loader.js'
|
||||
|
||||
bot.logger.info('Starting bot...')
|
||||
|
||||
bot.logger.info('Loading commands...')
|
||||
await importDirectory('./dist/commands')
|
||||
|
||||
bot.logger.info('Loading events...')
|
||||
await importDirectory('./dist/events')
|
||||
|
||||
await bot.start()
|
||||
7
examples/advanced/src/register-commands.ts
Normal file
7
examples/advanced/src/register-commands.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'dotenv/config'
|
||||
|
||||
import { bot } from './bot.js'
|
||||
import { updateApplicationCommands } from './utils/updateCommands.js'
|
||||
|
||||
bot.logger.info('Updating commands...')
|
||||
await updateApplicationCommands()
|
||||
15
examples/advanced/src/utils/loader.ts
Normal file
15
examples/advanced/src/utils/loader.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { logger } from '@discordeno/bot'
|
||||
import { readdir } from 'node:fs/promises'
|
||||
|
||||
export default async function importDirectory(folder: string): Promise<void> {
|
||||
const files = await readdir(folder, { recursive: true })
|
||||
|
||||
for (const filename of files) {
|
||||
if (!filename.endsWith('.js')) continue
|
||||
|
||||
// Using `file://` and `process.cwd()` to avoid weird issues with relative paths and/or Windows
|
||||
await import(`file://${process.cwd()}/${folder}/${filename}`).catch((x) =>
|
||||
logger.fatal(`Cannot import file (${folder}/${filename}) for reason:`, x),
|
||||
)
|
||||
}
|
||||
}
|
||||
20
examples/advanced/src/utils/permissions.ts
Normal file
20
examples/advanced/src/utils/permissions.ts
Normal file
@@ -0,0 +1,20 @@
|
||||
import { BitwisePermissionFlags, type Guild, type Member } from '@discordeno/bot'
|
||||
import assert from 'node:assert'
|
||||
|
||||
export async function calculateMemberPermissions(guild: Guild, member: Member): Promise<bigint> {
|
||||
if (member.id === guild.ownerId) return 8n
|
||||
|
||||
let permissions = guild.roles.get(guild.id)?.permissions.bitfield
|
||||
const rolePerms = member.roles.map((x) => guild.roles.get(x)?.permissions.bitfield).filter((x): x is bigint => x !== undefined)
|
||||
|
||||
// Small hack to avoid calling assert with 0n
|
||||
if (permissions === undefined) assert(permissions)
|
||||
|
||||
for (const rolePerm of rolePerms) {
|
||||
permissions |= rolePerm
|
||||
}
|
||||
|
||||
if ((permissions & BigInt(BitwisePermissionFlags.ADMINISTRATOR)) === BigInt(BitwisePermissionFlags.ADMINISTRATOR)) return 8n
|
||||
|
||||
return permissions
|
||||
}
|
||||
6
examples/advanced/src/utils/updateCommands.ts
Normal file
6
examples/advanced/src/utils/updateCommands.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import { bot } from '../bot.js'
|
||||
import { commands } from '../commands.js'
|
||||
|
||||
export async function updateApplicationCommands(): Promise<void> {
|
||||
await bot.helpers.upsertGlobalApplicationCommands(commands.array())
|
||||
}
|
||||
14
examples/advanced/tsconfig.json
Normal file
14
examples/advanced/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"moduleResolution": "node",
|
||||
"skipDefaultLibCheck": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"incremental": true
|
||||
}
|
||||
}
|
||||
2124
examples/advanced/yarn.lock
Normal file
2124
examples/advanced/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,2 +1 @@
|
||||
BOT_TOKEN=''
|
||||
DEV_GUILD_ID=''
|
||||
32
examples/beginner/.gitignore
vendored
Normal file
32
examples/beginner/.gitignore
vendored
Normal file
@@ -0,0 +1,32 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# build
|
||||
dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
24
examples/beginner/.swcrc
Normal file
24
examples/beginner/.swcrc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"decorators": true,
|
||||
"dynamicImport": true
|
||||
},
|
||||
"transform": {
|
||||
"legacyDecorator": true,
|
||||
"decoratorMetadata": true
|
||||
},
|
||||
"target": "es2022",
|
||||
"keepClassNames": true,
|
||||
"loose": true
|
||||
},
|
||||
"module": {
|
||||
"type": "es6",
|
||||
"strict": false,
|
||||
"strictMode": true,
|
||||
"lazy": false,
|
||||
"noInterop": false
|
||||
}
|
||||
}
|
||||
@@ -1,18 +1,20 @@
|
||||
# Beginner Bot Template
|
||||
|
||||
This template is designed for the beginner developer to start coding discord bots.
|
||||
This template is designed for beginners to start coding discord bots.
|
||||
|
||||
Make sure to install the latest version when you use it.
|
||||
This template includes caching (using `dd-cache-proxy`) and support for slash subcommands.
|
||||
This template also includes a /ping command to show the bot latency
|
||||
|
||||
## Setup
|
||||
|
||||
- [Click here](https://github.com/discordeno/template/generate) to make your own copy.
|
||||
- Delete all the template folders except the beginner folder.
|
||||
- Move all files from this folder to the root of the project.
|
||||
- You may encounter an issue with README.md file but force move the files to the root of the project.
|
||||
- Rename the .env.example file to .env OR create a new .env file and copy the example file code to this new file.
|
||||
- Download the source
|
||||
- Install the dependencies using `yarn`
|
||||
- Copy the .env.example file and rename it to .env
|
||||
- Fill out the .env file
|
||||
|
||||
## Run Bot
|
||||
|
||||
- deno run -A mod.ts
|
||||
- run `yarn` to install the dependencies
|
||||
- run `yarn build` to build the source
|
||||
- run `node dist/register-commands.js` to register the slash commands
|
||||
- run `yarn start` to run the bot
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
import { configs } from './configs.ts.js'
|
||||
import type { BotWithCache, BotWithHelpersPlugin } from './deps.ts.js'
|
||||
import {
|
||||
Collection,
|
||||
createBot,
|
||||
enableCachePlugin,
|
||||
enableCacheSweepers,
|
||||
enableHelpersPlugin,
|
||||
enablePermissionsPlugin,
|
||||
GatewayIntents,
|
||||
} from './deps.ts.js'
|
||||
import type { Command } from './src/types/commands.ts.js'
|
||||
|
||||
// MAKE THE BASIC BOT OBJECT
|
||||
const bot = createBot({
|
||||
token: configs.token,
|
||||
botId: configs.botId,
|
||||
intents: GatewayIntents.Guilds,
|
||||
events: {},
|
||||
})
|
||||
|
||||
// ENABLE ALL THE PLUGINS THAT WILL HELP MAKE IT EASIER TO CODE YOUR BOT
|
||||
enableHelpersPlugin(bot)
|
||||
enableCachePlugin(bot)
|
||||
enableCacheSweepers(bot as BotWithCache)
|
||||
enablePermissionsPlugin(bot as BotWithCache)
|
||||
|
||||
export interface BotClient extends BotWithCache<BotWithHelpersPlugin> {
|
||||
commands: Collection<string, Command>
|
||||
}
|
||||
|
||||
// THIS IS THE BOT YOU WANT TO USE EVERYWHERE IN YOUR CODE! IT HAS EVERYTHING BUILT INTO IT!
|
||||
export const Bot = bot as BotClient
|
||||
// PREPARE COMMANDS HOLDER
|
||||
Bot.commands = new Collection()
|
||||
@@ -1,19 +0,0 @@
|
||||
import { dotEnvConfig } from './deps.ts.js'
|
||||
|
||||
// Get the .env file that the user should have created, and get the token
|
||||
const env = dotEnvConfig({ export: true, path: './.env' })
|
||||
const token = env.BOT_TOKEN || ''
|
||||
|
||||
export interface Config {
|
||||
token: string
|
||||
botId: bigint
|
||||
}
|
||||
|
||||
export const configs = {
|
||||
/** Get token from ENV variable */
|
||||
token,
|
||||
/** Get the BotId from the token */
|
||||
botId: BigInt(atob(token.split('.')[0])),
|
||||
/** The server id where you develop your bot and want dev commands created. */
|
||||
devGuildId: BigInt(env.DEV_GUILD_ID!),
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
export * from 'https://deno.land/x/discordeno@17.0.0/mod.ts'
|
||||
export * from 'https://deno.land/x/discordeno@17.0.0/plugins/mod.ts'
|
||||
|
||||
// Terminal Colors!
|
||||
export * from 'https://deno.land/std@0.117.0/fmt/colors.ts'
|
||||
// Get data from .env files
|
||||
export { config as dotEnvConfig } from 'https://deno.land/x/dotenv@v3.1.0/mod.ts'
|
||||
// Database, thx Tri!
|
||||
export { decode as KwikDecode, encode as KwikEncode, Kwik } from 'https://deno.land/x/kwik@v1.3.1/mod.ts'
|
||||
@@ -1,25 +0,0 @@
|
||||
import { startBot } from './deps.ts.js'
|
||||
import log from './src/utils/logger.ts.js'
|
||||
import { fileLoader, importDirectory } from './src/utils/loader.ts.js'
|
||||
import { updateApplicationCommands } from './src/utils/updateCommands.ts.js'
|
||||
// setup db
|
||||
import './src/database/mod.ts.js'
|
||||
import { Bot } from './bot.ts.js'
|
||||
|
||||
log.info('Starting bot...')
|
||||
|
||||
// Forces deno to read all the files which will fill the commands/inhibitors cache etc.
|
||||
await Promise.all(
|
||||
[
|
||||
'./src/commands',
|
||||
'./src/events',
|
||||
// "./src/tasks",
|
||||
].map((path) => importDirectory(Deno.realPathSync(path))),
|
||||
)
|
||||
await fileLoader()
|
||||
|
||||
// UPDATES YOUR COMMANDS TO LATEST COMMANDS
|
||||
await updateApplicationCommands()
|
||||
|
||||
// STARTS THE CONNECTION TO DISCORD
|
||||
await startBot(Bot)
|
||||
27
examples/beginner/package.json
Normal file
27
examples/beginner/package.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"name": "dd-beginner-bot",
|
||||
"version": "1.0.0",
|
||||
"description": "An example bot for beginner developers to start coding discord bots.",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"license": "ISC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.0.2",
|
||||
"scripts": {
|
||||
"start": "node dist/index.js",
|
||||
"build": "swc src --strip-leading-paths --delete-dir-on-start --out-dir dist",
|
||||
"setup-dd": ""
|
||||
},
|
||||
"dependencies": {
|
||||
"@discordeno/bot": "19.0.0-next.92bf166",
|
||||
"chalk": "^5.3.0",
|
||||
"dd-cache-proxy": "^2.1.1",
|
||||
"dotenv": "^16.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.3.12",
|
||||
"@swc/core": "^1.6.3",
|
||||
"@types/node": "^20.14.6",
|
||||
"typescript": "^5.5.2"
|
||||
}
|
||||
}
|
||||
32
examples/beginner/src/bot.ts
Normal file
32
examples/beginner/src/bot.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
import { Intents, createBot } from '@discordeno/bot'
|
||||
import { createProxyCache } from 'dd-cache-proxy'
|
||||
import { configs } from './config.js'
|
||||
|
||||
export const bot = createProxyCache(
|
||||
createBot({
|
||||
token: configs.token,
|
||||
intents: Intents.Guilds,
|
||||
}),
|
||||
{
|
||||
desiredProps: {
|
||||
guilds: ['id', 'name'],
|
||||
},
|
||||
cacheInMemory: {
|
||||
guilds: true,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
)
|
||||
|
||||
// Setup desired proprieties
|
||||
bot.transformers.desiredProperties.interaction.id = true
|
||||
bot.transformers.desiredProperties.interaction.type = true
|
||||
bot.transformers.desiredProperties.interaction.data = true
|
||||
bot.transformers.desiredProperties.interaction.user = true
|
||||
bot.transformers.desiredProperties.interaction.token = true
|
||||
bot.transformers.desiredProperties.interaction.guildId = true
|
||||
|
||||
bot.transformers.desiredProperties.guild.id = true
|
||||
bot.transformers.desiredProperties.guild.name = true
|
||||
|
||||
bot.transformers.desiredProperties.user.username = true
|
||||
26
examples/beginner/src/commands.ts
Normal file
26
examples/beginner/src/commands.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Collection, type ApplicationCommandOption, type ApplicationCommandTypes, type Interaction } from '@discordeno/bot'
|
||||
|
||||
export const commands = new Collection<string, Command>()
|
||||
|
||||
export function createCommand(command: Command): void {
|
||||
commands.set(command.name, command)
|
||||
}
|
||||
|
||||
export interface Command {
|
||||
name: string
|
||||
description: string
|
||||
usage?: string[]
|
||||
options?: ApplicationCommandOption[]
|
||||
type: ApplicationCommandTypes
|
||||
/** Defaults to `Guild` */
|
||||
scope?: 'Global' | 'Guild'
|
||||
execute: (interaction: Interaction) => unknown
|
||||
subcommands?: Array<SubCommandGroup | SubCommand>
|
||||
}
|
||||
|
||||
export type SubCommand = Omit<Command, 'subcommands'>
|
||||
|
||||
export interface SubCommandGroup {
|
||||
name: string
|
||||
subCommands: SubCommand[]
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import { Bot } from '../../bot.ts.js'
|
||||
import type { Command } from '../types/commands.ts.js'
|
||||
|
||||
export function createCommand(command: Command) {
|
||||
Bot.commands.set(command.name, command)
|
||||
}
|
||||
@@ -1,18 +1,15 @@
|
||||
import { ApplicationCommandTypes, InteractionResponseTypes } from '../../deps.ts.js'
|
||||
import { snowflakeToTimestamp } from '../utils/helpers.ts.js'
|
||||
import { createCommand } from './mod.ts.js'
|
||||
import { ApplicationCommandTypes, snowflakeToTimestamp } from '@discordeno/bot'
|
||||
import { createCommand } from '../commands.js'
|
||||
import { humanizeMilliseconds } from '../utils/helpers.js'
|
||||
|
||||
createCommand({
|
||||
name: 'ping',
|
||||
description: 'Ping the Bot!',
|
||||
type: ApplicationCommandTypes.ChatInput,
|
||||
execute: async (Bot, interaction) => {
|
||||
scope: 'Global',
|
||||
async execute(interaction) {
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id)
|
||||
await Bot.helpers.sendInteractionResponse(interaction.id, interaction.token, {
|
||||
type: InteractionResponseTypes.ChannelMessageWithSource,
|
||||
data: {
|
||||
content: `🏓 Pong! ${ping}ms`,
|
||||
},
|
||||
})
|
||||
|
||||
await interaction.respond(`🏓 Pong! Ping ${ping}ms (${humanizeMilliseconds(ping)})`)
|
||||
},
|
||||
})
|
||||
|
||||
12
examples/beginner/src/config.ts
Normal file
12
examples/beginner/src/config.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
const token = process.env.BOT_TOKEN
|
||||
|
||||
if (!token) throw new Error('Missing BOT_TOKEN environment variable')
|
||||
|
||||
export const configs: Config = {
|
||||
/** Get token from ENV variable */
|
||||
token,
|
||||
}
|
||||
|
||||
export interface Config {
|
||||
token: string
|
||||
}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Kwik, KwikDecode, KwikEncode } from '../../deps.ts.js'
|
||||
import { logger } from '../utils/logger.ts.js'
|
||||
|
||||
const log = logger({ name: 'DB Manager' })
|
||||
|
||||
log.info('Initializing Database')
|
||||
|
||||
const kwik = new Kwik()
|
||||
|
||||
// Add BigInt Support
|
||||
kwik.msgpackExtensionCodec.register({
|
||||
type: 0,
|
||||
encode: (object: unknown): Uint8Array | null => {
|
||||
if (typeof object === 'bigint') {
|
||||
if (object <= Number.MAX_SAFE_INTEGER && object >= Number.MIN_SAFE_INTEGER) {
|
||||
return KwikEncode(parseInt(object.toString(), 10), {})
|
||||
} else {
|
||||
return KwikEncode(object.toString(), {})
|
||||
}
|
||||
} else {
|
||||
return null
|
||||
}
|
||||
},
|
||||
decode: (data: Uint8Array) => {
|
||||
return BigInt(KwikDecode(data, {}) as string)
|
||||
},
|
||||
})
|
||||
|
||||
// Initialize the Database
|
||||
await kwik.init()
|
||||
|
||||
log.info('Database Initialized!')
|
||||
4
examples/beginner/src/events/guildCreate.ts
Normal file
4
examples/beginner/src/events/guildCreate.ts
Normal file
@@ -0,0 +1,4 @@
|
||||
import { bot } from '../bot.js'
|
||||
import { updateGuildCommands } from '../utils/helpers.js'
|
||||
|
||||
bot.events.guildCreate = async (guild) => await updateGuildCommands(bot, guild)
|
||||
@@ -1,14 +1,94 @@
|
||||
import { Bot } from '../../bot.ts.js'
|
||||
import { InteractionTypes } from '../../deps.ts.js'
|
||||
import log from '../utils/logger.ts.js'
|
||||
import { ApplicationCommandOptionTypes, hasProperty, type Guild } from '@discordeno/bot'
|
||||
import chalk from 'chalk'
|
||||
import { bot } from '../bot.js'
|
||||
import { commands } from '../commands.js'
|
||||
import { getGuildFromId, isSubCommand, isSubCommandGroup } from '../utils/helpers.js'
|
||||
import { createLogger } from '../utils/logger.js'
|
||||
|
||||
Bot.events.interactionCreate = (_, interaction) => {
|
||||
if (!interaction.data) return
|
||||
const logger = createLogger({ name: 'Event: InteractionCreate' })
|
||||
|
||||
switch (interaction.type) {
|
||||
case InteractionTypes.ApplicationCommand:
|
||||
log.info(`[Application Command] ${interaction.data.name} command executed.`)
|
||||
Bot.commands.get(interaction.data.name!)?.execute(Bot, interaction)
|
||||
break
|
||||
bot.events.interactionCreate = async (interaction) => {
|
||||
if (!interaction.data || !interaction.id) return
|
||||
|
||||
let guildName = 'Direct Message'
|
||||
let guild = {} as Guild
|
||||
|
||||
// Set guild, if there was an error getting the guild, then just say it was a DM. (What else are we going to do?)
|
||||
if (interaction.guildId) {
|
||||
const guildOrVoid = await getGuildFromId(interaction.guildId).catch((err) => {
|
||||
logger.error(err)
|
||||
})
|
||||
|
||||
if (guildOrVoid) {
|
||||
guild = guildOrVoid
|
||||
guildName = guild.name
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(
|
||||
`[Command: ${chalk.bgYellow.black(interaction.data.name)} - ${chalk.bgBlack.white(`Trigger`)}] by @${interaction.user.username} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
|
||||
)
|
||||
|
||||
let command = commands.get(interaction.data.name)
|
||||
|
||||
if (!command) {
|
||||
logger.warn(
|
||||
`[Command: ${chalk.bgYellow.black(interaction.data.name)} - ${chalk.bgBlack.yellow(`Not Found`)}] by @${interaction.user.username} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
|
||||
)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
if (interaction.data.options?.[0]) {
|
||||
const optionType = interaction.data.options[0].type
|
||||
|
||||
if (optionType === ApplicationCommandOptionTypes.SubCommandGroup) {
|
||||
// Check if command has subcommand and handle types
|
||||
if (!command.subcommands) return
|
||||
|
||||
// Try to find the subcommand group
|
||||
const subCommandGroup = command.subcommands?.find((command) => command.name === interaction.data?.options?.[0].name)
|
||||
if (!subCommandGroup) return
|
||||
|
||||
if (isSubCommand(subCommandGroup)) return
|
||||
|
||||
// Get name of the command which we are looking for
|
||||
const targetCmdName = interaction.data.options?.[0].options?.[0].name ?? interaction.data.options?.[0].options?.[0].name
|
||||
if (!targetCmdName) return
|
||||
|
||||
// Try to find the command
|
||||
command = subCommandGroup.subCommands.find((c) => c.name === targetCmdName)
|
||||
}
|
||||
|
||||
if (optionType === ApplicationCommandOptionTypes.SubCommand) {
|
||||
// Check if command has subcommand and handle types
|
||||
if (!command?.subcommands) return
|
||||
|
||||
// Try to find the command
|
||||
const found = command.subcommands.find((command) => command.name === interaction.data?.options?.[0].name)
|
||||
if (!found) return
|
||||
|
||||
if (isSubCommandGroup(found)) return
|
||||
|
||||
command = found
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if (!command) throw new Error('Not command could be found')
|
||||
|
||||
await command.execute(interaction)
|
||||
|
||||
logger.info(
|
||||
`[Command: ${chalk.bgYellow.black(interaction.data.name)} - ${chalk.bgBlack.green(`Success`)}] by @${interaction.user.username} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
|
||||
)
|
||||
} catch (err) {
|
||||
logger.error(
|
||||
`[Command: ${chalk.bgYellow.black(interaction.data.name)} - ${chalk.bgBlack.red(`Error`)}] by @${interaction.user.username} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
|
||||
)
|
||||
|
||||
if (typeof err !== 'object' || !err || !hasProperty(err, 'message') || err.message === 'Not command could be found') return
|
||||
|
||||
logger.error(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
import { Bot } from '../../bot.ts.js'
|
||||
import log from '../utils/logger.ts.js'
|
||||
import { ActivityTypes } from '@discordeno/bot'
|
||||
import { bot } from '../bot.js'
|
||||
import { createLogger } from '../utils/logger.js'
|
||||
|
||||
Bot.events.ready = (_, payload) => {
|
||||
log.info(`[READY] Shard ID ${payload.shardId} of ${Bot.gateway.lastShardId + 1} shards is ready!`)
|
||||
const logger = createLogger({ name: 'Event: Ready' })
|
||||
|
||||
if (payload.shardId === Bot.gateway.lastShardId) {
|
||||
botFullyReady()
|
||||
}
|
||||
}
|
||||
|
||||
// This function lets you run custom code when all your bot's shards are online.
|
||||
function botFullyReady() {
|
||||
// DO STUFF YOU WANT HERE ONCE BOT IS FULLY ONLINE.
|
||||
log.info('[READY] Bot is fully online.')
|
||||
bot.events.ready = async ({ shardId }) => {
|
||||
logger.info('Bot Ready')
|
||||
|
||||
await bot.gateway.editShardStatus(shardId, {
|
||||
status: 'online',
|
||||
activities: [
|
||||
{
|
||||
name: 'Discordeno is the Best Lib',
|
||||
type: ActivityTypes.Game,
|
||||
timestamps: {
|
||||
start: Date.now(),
|
||||
},
|
||||
},
|
||||
],
|
||||
})
|
||||
}
|
||||
|
||||
15
examples/beginner/src/index.ts
Normal file
15
examples/beginner/src/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import 'dotenv/config'
|
||||
|
||||
import { bot } from './bot.js'
|
||||
import importDirectory from './utils/loader.js'
|
||||
import logger from './utils/logger.js'
|
||||
|
||||
logger.info('Starting bot...')
|
||||
|
||||
logger.info('Loading commands...')
|
||||
await importDirectory('./dist/commands')
|
||||
|
||||
logger.info('Loading events...')
|
||||
await importDirectory('./dist/events')
|
||||
|
||||
await bot.start()
|
||||
7
examples/beginner/src/register-commands.ts
Normal file
7
examples/beginner/src/register-commands.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import 'dotenv/config'
|
||||
|
||||
import { bot } from './bot.js'
|
||||
import { updateCommands } from './utils/helpers.js'
|
||||
|
||||
bot.logger.info('Updating commands...')
|
||||
await updateCommands()
|
||||
@@ -1,3 +0,0 @@
|
||||
// This file will export all of the types in this directory.
|
||||
|
||||
export * from './commands.ts.js'
|
||||
@@ -1,3 +1,101 @@
|
||||
export function snowflakeToTimestamp(id: bigint) {
|
||||
return Number(id / 4194304n + 1420070400000n)
|
||||
import { hasProperty, type Bot, type CreateApplicationCommand, type Guild } from '@discordeno/bot'
|
||||
import { bot } from '../bot.js'
|
||||
import { commands, type SubCommand, type SubCommandGroup } from '../commands.js'
|
||||
import { createLogger } from './logger.js'
|
||||
|
||||
const logger = createLogger({ name: 'Helpers' })
|
||||
|
||||
/** This function will update all commands, or the defined scope */
|
||||
export async function updateCommands(scope?: 'Guild' | 'Global'): Promise<void> {
|
||||
const globalCommands: Array<MakeRequired<CreateApplicationCommand, 'name'>> = []
|
||||
const perGuildCommands: Array<MakeRequired<CreateApplicationCommand, 'name'>> = []
|
||||
|
||||
for (const command of commands.values()) {
|
||||
if (command.scope === 'Guild') {
|
||||
perGuildCommands.push({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
type: command.type,
|
||||
options: command.options ? command.options : undefined,
|
||||
})
|
||||
} else {
|
||||
globalCommands.push({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
type: command.type,
|
||||
options: command.options ? command.options : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (globalCommands.length && (scope === 'Global' || scope === undefined)) {
|
||||
logger.info('Updating Global Commands, changes should apply in short...')
|
||||
await bot.helpers.upsertGlobalApplicationCommands(globalCommands).catch(logger.error)
|
||||
}
|
||||
|
||||
if (perGuildCommands.length && (scope === 'Guild' || scope === undefined)) {
|
||||
await Promise.all(
|
||||
bot.cache.guilds.memory.map(async (guild: Guild) => {
|
||||
await bot.helpers.upsertGuildApplicationCommands(guild.id, perGuildCommands)
|
||||
}),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/** Update commands for a guild */
|
||||
export async function updateGuildCommands(bot: Bot, guild: Guild): Promise<void> {
|
||||
const perGuildCommands: Array<MakeRequired<CreateApplicationCommand, 'name'>> = []
|
||||
|
||||
for (const command of commands.values()) {
|
||||
if (command.scope === 'Guild') {
|
||||
perGuildCommands.push({
|
||||
name: command.name,
|
||||
description: command.description,
|
||||
type: command.type,
|
||||
options: command.options ? command.options : undefined,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
if (perGuildCommands.length) {
|
||||
await bot.helpers.upsertGuildApplicationCommands(guild.id, perGuildCommands)
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGuildFromId(guildId: bigint): Promise<Guild> {
|
||||
const cached = await bot.cache.guilds.get(guildId)
|
||||
|
||||
if (cached) return cached
|
||||
|
||||
return await bot.helpers.getGuild(guildId)
|
||||
}
|
||||
|
||||
export function humanizeMilliseconds(milliseconds: number): string {
|
||||
// Gets ms into seconds
|
||||
const time = milliseconds / 1000
|
||||
if (time < 1) return '< 1s'
|
||||
|
||||
const days = Math.floor(time / 86400)
|
||||
const hours = Math.floor((time % 86400) / 3600)
|
||||
const minutes = Math.floor(((time % 86400) % 3600) / 60)
|
||||
const seconds = Math.floor(((time % 86400) % 3600) % 60)
|
||||
|
||||
const dayString = days ? `${days}d ` : ''
|
||||
const hourString = hours ? `${hours}h ` : ''
|
||||
const minuteString = minutes ? `${minutes}m ` : ''
|
||||
const secondString = seconds ? `${seconds}s ` : ''
|
||||
|
||||
return `${dayString}${hourString}${minuteString}${secondString}`
|
||||
}
|
||||
|
||||
export function isSubCommand(data: SubCommand | SubCommandGroup): data is SubCommand {
|
||||
return !hasProperty(data, 'subCommands')
|
||||
}
|
||||
|
||||
export function isSubCommandGroup(data: SubCommand | SubCommandGroup): data is SubCommandGroup {
|
||||
return hasProperty(data, 'subCommands')
|
||||
}
|
||||
|
||||
type MakeRequired<TObj, TKey extends keyof TObj> = TObj & {
|
||||
[Key in TKey]-?: TObj[Key]
|
||||
}
|
||||
|
||||
@@ -1,40 +1,15 @@
|
||||
import log from './logger.ts.js'
|
||||
import { readdir } from 'node:fs/promises'
|
||||
import logger from './logger.js'
|
||||
|
||||
// Very important to make sure files are reloaded properly
|
||||
let uniqueFilePathCounter = 0
|
||||
let paths: string[] = []
|
||||
export default async function importDirectory(folder: string): Promise<void> {
|
||||
const files = await readdir(folder, { recursive: true })
|
||||
|
||||
/** This function allows reading all files in a folder. Useful for loading/reloading commands, monitors etc */
|
||||
export async function importDirectory(path: string) {
|
||||
path = path.replaceAll('\\', '/')
|
||||
const files = Deno.readDirSync(Deno.realPathSync(path))
|
||||
const folder = path.substring(path.indexOf('/src/') + 5)
|
||||
for (const filename of files) {
|
||||
if (!filename.endsWith('.js')) continue
|
||||
|
||||
if (!folder.includes('/')) log.info(`Loading ${folder}...`)
|
||||
|
||||
for (const file of files) {
|
||||
if (!file.name) continue
|
||||
|
||||
const currentPath = `${path}/${file.name}`
|
||||
if (file.isFile) {
|
||||
if (!currentPath.endsWith('.ts')) continue
|
||||
paths.push(
|
||||
`import "${Deno.mainModule.substring(0, Deno.mainModule.lastIndexOf('/'))}/${currentPath.substring(
|
||||
currentPath.indexOf('src/'),
|
||||
)}#${uniqueFilePathCounter}";`,
|
||||
// Using `file://` and `process.cwd()` to avoid weird issues with relative paths and/or Windows
|
||||
await import(`file://${process.cwd()}/${folder}/${filename}`).catch((x) =>
|
||||
logger.fatal(`Cannot import file (${folder}/${filename}) for reason:`, x),
|
||||
)
|
||||
continue
|
||||
}
|
||||
|
||||
await importDirectory(currentPath)
|
||||
}
|
||||
|
||||
uniqueFilePathCounter++
|
||||
}
|
||||
|
||||
/** Imports all everything in fileloader.ts */
|
||||
export async function fileLoader() {
|
||||
await Deno.writeTextFile('fileloader.ts', paths.join('\n').replaceAll('\\', '/'))
|
||||
await import(`${Deno.mainModule.substring(0, Deno.mainModule.lastIndexOf('/'))}/fileloader.ts#${uniqueFilePathCounter}`)
|
||||
paths = []
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// deno-lint-ignore-file no-explicit-any
|
||||
import { bold, cyan, gray, italic, red, yellow } from '../../deps.ts.js'
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-argument */
|
||||
import chalk from 'chalk'
|
||||
|
||||
export enum LogLevels {
|
||||
Debug,
|
||||
@@ -19,21 +19,15 @@ const prefixes = new Map<LogLevels, string>([
|
||||
|
||||
const noColor: (str: string) => string = (msg) => msg
|
||||
const colorFunctions = new Map<LogLevels, (str: string) => string>([
|
||||
[LogLevels.Debug, gray],
|
||||
[LogLevels.Info, cyan],
|
||||
[LogLevels.Warn, yellow],
|
||||
[LogLevels.Error, (str: string) => red(str)],
|
||||
[LogLevels.Fatal, (str: string) => red(bold(italic(str)))],
|
||||
[LogLevels.Debug, chalk.gray],
|
||||
[LogLevels.Info, chalk.cyan],
|
||||
[LogLevels.Warn, chalk.yellow],
|
||||
[LogLevels.Error, (str: string) => chalk.red(str)],
|
||||
[LogLevels.Fatal, (str: string) => chalk.red.bold.italic(str)],
|
||||
])
|
||||
|
||||
export function logger({
|
||||
logLevel = LogLevels.Info,
|
||||
name,
|
||||
}: {
|
||||
logLevel?: LogLevels
|
||||
name?: string
|
||||
} = {}) {
|
||||
function log(level: LogLevels, ...args: any[]) {
|
||||
export function createLogger({ logLevel = LogLevels.Info, name }: { logLevel?: LogLevels; name?: string } = {}): Logger {
|
||||
function log(level: LogLevels, ...args: any[]): void {
|
||||
if (level < logLevel) return
|
||||
|
||||
let color = colorFunctions.get(level)
|
||||
@@ -42,7 +36,7 @@ export function logger({
|
||||
const date = new Date()
|
||||
const log = [
|
||||
`[${date.toLocaleDateString()} ${date.toLocaleTimeString()}]`,
|
||||
color(prefixes.get(level) || 'DEBUG'),
|
||||
color(prefixes.get(level) ?? 'DEBUG'),
|
||||
name ? `${name} >` : '>',
|
||||
...args,
|
||||
]
|
||||
@@ -63,27 +57,27 @@ export function logger({
|
||||
}
|
||||
}
|
||||
|
||||
function setLevel(level: LogLevels) {
|
||||
function setLevel(level: LogLevels): void {
|
||||
logLevel = level
|
||||
}
|
||||
|
||||
function debug(...args: any[]) {
|
||||
function debug(...args: any[]): void {
|
||||
log(LogLevels.Debug, ...args)
|
||||
}
|
||||
|
||||
function info(...args: any[]) {
|
||||
function info(...args: any[]): void {
|
||||
log(LogLevels.Info, ...args)
|
||||
}
|
||||
|
||||
function warn(...args: any[]) {
|
||||
function warn(...args: any[]): void {
|
||||
log(LogLevels.Warn, ...args)
|
||||
}
|
||||
|
||||
function error(...args: any[]) {
|
||||
function error(...args: any[]): void {
|
||||
log(LogLevels.Error, ...args)
|
||||
}
|
||||
|
||||
function fatal(...args: any[]) {
|
||||
function fatal(...args: any[]): void {
|
||||
log(LogLevels.Fatal, ...args)
|
||||
}
|
||||
|
||||
@@ -98,5 +92,15 @@ export function logger({
|
||||
}
|
||||
}
|
||||
|
||||
export const log = logger({ name: 'Main' })
|
||||
export default log
|
||||
export const logger = createLogger({ name: 'Main' })
|
||||
export default logger
|
||||
|
||||
export interface Logger {
|
||||
log: (level: LogLevels, ...args: any[]) => void
|
||||
debug: (...args: any[]) => void
|
||||
info: (...args: any[]) => void
|
||||
warn: (...args: any[]) => void
|
||||
error: (...args: any[]) => void
|
||||
fatal: (...args: any[]) => void
|
||||
setLevel: (level: LogLevels) => void
|
||||
}
|
||||
|
||||
@@ -1,19 +0,0 @@
|
||||
import { Bot } from '../../bot.ts.js'
|
||||
import { configs } from '../../configs.ts.js'
|
||||
|
||||
export async function updateApplicationCommands() {
|
||||
await Bot.helpers.upsertGlobalApplicationCommands(
|
||||
Bot.commands
|
||||
// ONLY GLOBAL COMMANDS
|
||||
.filter((command) => !command.devOnly)
|
||||
.array(),
|
||||
)
|
||||
|
||||
await Bot.helpers.upsertGuildApplicationCommands(
|
||||
configs.devGuildId,
|
||||
Bot.commands
|
||||
// ONLY GLOBAL COMMANDS
|
||||
.filter((command) => !!command.devOnly)
|
||||
.array(),
|
||||
)
|
||||
}
|
||||
14
examples/beginner/tsconfig.json
Normal file
14
examples/beginner/tsconfig.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "es2022",
|
||||
"module": "es2022",
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"isolatedModules": true,
|
||||
"moduleResolution": "node",
|
||||
"skipDefaultLibCheck": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"incremental": true
|
||||
}
|
||||
}
|
||||
2133
examples/beginner/yarn.lock
Normal file
2133
examples/beginner/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
31
examples/bigbot/.dockerignore
Normal file
31
examples/bigbot/.dockerignore
Normal file
@@ -0,0 +1,31 @@
|
||||
# Include any files or directories that you don't want to be copied to your
|
||||
# container here (e.g., local build artifacts, temporary files, etc.).
|
||||
#
|
||||
# For more help, visit the .dockerignore file reference guide at
|
||||
# https://docs.docker.com/go/build-context-dockerignore/
|
||||
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# Git files
|
||||
.git
|
||||
.gitignore
|
||||
|
||||
# Docker files
|
||||
.dockerignore
|
||||
docker-compose*
|
||||
compose.y*ml
|
||||
Dockerfile*
|
||||
|
||||
# Local build files
|
||||
node_modules
|
||||
dist
|
||||
.yarn
|
||||
|
||||
LICENSE
|
||||
README.md
|
||||
@@ -1,104 +1,142 @@
|
||||
# For Prisma use. Remember to remove the [ and ]
|
||||
DATABASE_URL=postgres:#[username]:[password]@[host]:[port]/[db]
|
||||
|
||||
# For other configs, update src/configs.ts
|
||||
|
||||
#
|
||||
# General Configurations
|
||||
# Whether or not this process is a local development version. Change to false for the main production bot. */
|
||||
# SETUP-DD-TEMP: Change this to false in your server for production bot. Keep true in local testing.
|
||||
#
|
||||
|
||||
# Whether or not this process is a local development version
|
||||
# In production this value should be set to false
|
||||
# TEMPLATE-SETUP: When deploying, set this value to false
|
||||
DEVELOPMENT=true
|
||||
|
||||
# The server id where you develop/test the bot. */
|
||||
# SETUP-DD-TEMP: Change the id to a server where you develop the bot privately.
|
||||
# The server id where you develop/test the bot
|
||||
# TEMPLATE-SETUP: Add the id to a server where you develop the bot
|
||||
DEV_SERVER_ID=
|
||||
|
||||
# The discord bot token, without the BOT prefix. */
|
||||
# SETUP-DD-TEMP: Add the bot token here.
|
||||
# The discord bot token
|
||||
# NOTE: It should not be prefixed with Bot
|
||||
# TEMPLATE-SETUP: Add the bot token here.
|
||||
DISCORD_TOKEN=
|
||||
|
||||
# Bot Configurations
|
||||
# The secret passcode that the bot code (event handler) is listening for. This is used to prevent someone else from trying to send malicious messages to your bot. */
|
||||
# SETUP-DD-TEMP: Add a secret passcode here.
|
||||
EVENT_HANDLER_AUTHORIZATION=SuperSecretPassword
|
||||
#
|
||||
# Bot Configuration
|
||||
#
|
||||
|
||||
# The host where the event handler will run. Must follow https:#nodejs.org/api/net.html#serverlistenoptions-callback. */
|
||||
# SETUP-DD-TEMP: Set the event handler's host here.
|
||||
# NOTE: With "bot code" we refer to the code that will handle the events
|
||||
|
||||
# The secret passcode that the bot code is checking for
|
||||
# This is used to prevent someone else from trying to send malicious events to your bot
|
||||
# TEMPLATE-SETUP: Add a secret passcode here. It can be whatever you want
|
||||
EVENT_HANDLER_AUTHORIZATION=
|
||||
|
||||
# The host where the event handler will run
|
||||
# Will be used together with EVENT_HANDLER_PORT to compose the HTTP url to send the events to
|
||||
# TEMPLATE-SETUP: Set the event handler's host here
|
||||
EVENT_HANDLER_HOST=localhost
|
||||
|
||||
# The port where the event handler is listening for events. */
|
||||
# SETUP-DD-TEMP: Set the desired port where events will be sent to be processed.
|
||||
# The port where the event handler will listening for events
|
||||
# TEMPLATE-SETUP: Set the port where events will be sent
|
||||
EVENT_HANDLER_PORT=8081
|
||||
|
||||
# The full webhook url where the bot can send errors to alert you that the bot is missing translations. */
|
||||
# SETUP-DD-TEMP: Set a full discord webhook url here.
|
||||
MISSING_TRANSLATION_WEBHOOK=
|
||||
|
||||
# The full webhook url where the bot can send errors to alert you that the bot is throwing errors. */
|
||||
# SETUP-DD-TEMP: Set a full discord webhook url here.
|
||||
# The full webhook url where the bot can send errors to alert you that the bot is throwing errors.
|
||||
# TEMPLATE-SETUP: Add the full discord webhook url
|
||||
BUGS_ERRORS_REPORT_WEBHOOK=
|
||||
|
||||
#
|
||||
# Rest Proxy Configurations
|
||||
# The authorization code that the REST proxy will check for to make sure the requests are coming from you. */
|
||||
# SETUP-DD-TEMP: Add a secret passcode here.
|
||||
REST_AUTHORIZATION=password123
|
||||
#
|
||||
|
||||
# The host where the REST proxy will run. Must follow https:#nodejs.org/api/net.html#serverlistenoptions-callback. */
|
||||
# SETUP-DD-TEMP: Set the REST proxy's host here.
|
||||
# The passcode that the REST proxy is checking for
|
||||
# This is used to prevent someone else from trying to send malicious API requests from your bot
|
||||
# TEMPLATE-SETUP: Add a secret passcode here. It can be whatever you want
|
||||
REST_AUTHORIZATION=
|
||||
|
||||
# The host where the REST proxy will run
|
||||
# Will be used together with REST_PORT to compose the HTTP url to send the API requests to
|
||||
# TEMPLATE-SETUP: Set the REST proxy's host here
|
||||
REST_HOST=localhost
|
||||
|
||||
# The port that will run the REST proxy. */
|
||||
# SETUP-DD-TEMP: Choose the port number here that will be used for the REST proxy.
|
||||
# The port where the REST proxy will listen for API requests
|
||||
# TEMPLATE-SETUP: Set the port where API requests will be sent
|
||||
REST_PORT=8000
|
||||
|
||||
#
|
||||
# Gateway Proxy Configurations
|
||||
#
|
||||
|
||||
# The amount of shards to start. Useful with multiple servers where each server is handling a portion of your bot. */
|
||||
# SETUP-DD-TEMP: To start all bots, leave it as undefined. Specify he number of shards this process should handle.
|
||||
# The amount of shards to start
|
||||
# Useful with multiple servers where each server is handling a portion of your bot
|
||||
# OPTIONAL: You can leave this value unspecified if you want this server to manage all shards
|
||||
# TEMPLATE-SETUP: If you have separate servers, add the number of shards this process should handle
|
||||
TOTAL_SHARDS=
|
||||
|
||||
# The amount of shards to start per worker. */
|
||||
# SETUP-DD-TEMP: Choose how many shards to start per worker. If you are not sure just stick to 16.
|
||||
# The amount of shards to start per worker.
|
||||
# NOTE: If you are not sure just stick to 16
|
||||
# TEMPLATE-SETUP: Set how many shards to start per worker
|
||||
SHARDS_PER_WORKER=16
|
||||
|
||||
# The total amount of workers to start. Generally this should be equal to the number of cores your server has. */
|
||||
# SETUP-DD-TEMP: Choose how many workers to start up. If you are not sure, check how many cores your server has.
|
||||
# The total amount of workers to start.
|
||||
# NOTE: Generally this should be equal to the number of cores your server has
|
||||
# TEMPLATE-SETUP: Choose how many workers to start up
|
||||
TOTAL_WORKERS=4
|
||||
|
||||
# The secret passcode that the gateway is listening for. This is used to prevent someone else from trying to send malicious messages to your bot. */
|
||||
# SETUP-DD-TEMP: Add a secret passcode here.
|
||||
# The passcode that the gateway is checking for
|
||||
# This is used to prevent someone else from trying to send malicious messages to your bot
|
||||
# TEMPLATE-SETUP: Set a secret passcode here. It can be whatever you want
|
||||
GATEWAY_AUTHORIZATION=
|
||||
|
||||
# The host where the gateway will run. Must follow https:#nodejs.org/api/net.html#serverlistenoptions-callback. */
|
||||
# SETUP-DD-TEMP: Set the gateways's host here.
|
||||
# The host where the gateway will run
|
||||
# Will be used together with GATEWAY_PORT to compose the HTTP url to send the gateway messages to
|
||||
# TEMPLATE-SETUP: Set the gateway's host here
|
||||
GATEWAY_HOST=localhost
|
||||
|
||||
# The port where the gateway will run. This is where the bot will send its messages to the gateway. */
|
||||
# SETUP-DD-TEMP: Set the gateways's port here.
|
||||
# The port where the gateway will listen for gateway messages
|
||||
# TEMPLATE-SETUP: Set the port where gateway messages will be sent
|
||||
GATEWAY_PORT=8080
|
||||
|
||||
# Messsage queue / RabbitMQ configuration
|
||||
# enable using messages queue to send messages from gateway to bot
|
||||
#
|
||||
# Message queue (RabbitMQ configuration)
|
||||
#
|
||||
|
||||
# Whatever to queue messages from the gateway to bot
|
||||
# NOTE: If this is set to true, all other configuration in this section are requried
|
||||
# NOTE: if this is set to false, gateway messages will be sent directly to the bot code, and will fail if the bot code is not running
|
||||
MESSAGEQUEUE_ENABLE=false
|
||||
|
||||
# The url of the message queue
|
||||
MESSAGEQUEUE_URL=rabbitmq:5672
|
||||
# The url of the RabbitMQ instance
|
||||
MESSAGEQUEUE_URL=localhost:5672
|
||||
|
||||
# username and password for the message queue
|
||||
MESSAGEQUEUE_USERNAME=guest
|
||||
MESSAGEQUEUE_PASSWORD=guest
|
||||
# Username for the authentication against the RabbitMQ instance
|
||||
MESSAGEQUEUE_USERNAME=
|
||||
|
||||
# Database Configurations
|
||||
# Password for the authentication against the RabbitMQ instance
|
||||
MESSAGEQUEUE_PASSWORD=
|
||||
|
||||
# These INFLUX configs are only if you wish to enable analytics. */
|
||||
# SETUP-DD-TEMP: This is optional. If you want to build analytics, add influxdb here.
|
||||
#
|
||||
# Analytics (InfluxDB configuration)
|
||||
#
|
||||
|
||||
# NOTE: This entire section is optional
|
||||
# TEMPLATE-SETUP: If you want to enable analytics, add the the following values
|
||||
|
||||
# The InfluxDB organization
|
||||
INFLUX_ORG=
|
||||
|
||||
# The InfluxDB bucket
|
||||
INFLUX_BUCKET=
|
||||
|
||||
# The InfluxDB secret API token
|
||||
# NOTE: this may need to be in quotes ("...") if it contains the = sign
|
||||
INFLUX_TOKEN=
|
||||
INFLUX_URL=http://influxdb:8086
|
||||
|
||||
# The InfluxDB Instance url
|
||||
INFLUX_URL=http://localhost:8086
|
||||
|
||||
#
|
||||
# Docker InfluxDB
|
||||
#
|
||||
|
||||
DOCKER_INFLUXDB_INIT_MODE=setup
|
||||
DOCKER_INFLUXDB_INIT_USERNAME=skillz
|
||||
DOCKER_INFLUXDB_INIT_PASSWORD=ILoveskillz
|
||||
DOCKER_INFLUXDB_INIT_USERNAME=discordeno
|
||||
DOCKER_INFLUXDB_INIT_PASSWORD=discordeno
|
||||
DOCKER_INFLUXDB_INIT_ORG=discordeno
|
||||
DOCKER_INFLUXDB_INIT_BUCKET=discordeno
|
||||
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=skillzPrefersID
|
||||
DOCKER_INFLUXDB_INIT_ADMIN_TOKEN=discordeno
|
||||
|
||||
30
examples/bigbot/.gitignore
vendored
30
examples/bigbot/.gitignore
vendored
@@ -1,2 +1,32 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
node_modules
|
||||
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
|
||||
# build
|
||||
dist
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# local env files
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
@@ -20,6 +20,5 @@
|
||||
"strictMode": true,
|
||||
"lazy": false,
|
||||
"noInterop": false
|
||||
},
|
||||
"sourceMaps": true
|
||||
}
|
||||
}
|
||||
4
examples/bigbot/.yarnrc.yml
Normal file
4
examples/bigbot/.yarnrc.yml
Normal file
@@ -0,0 +1,4 @@
|
||||
# This file is needed or else when yarn runs in `docker build` will attempt to use PnP
|
||||
compressionLevel: mixed
|
||||
enableGlobalCache: false
|
||||
nodeLinker: node-modules
|
||||
@@ -1,37 +1,75 @@
|
||||
FROM node:16.18.0-alpine3.16 AS deps
|
||||
WORKDIR /app
|
||||
COPY package.json package-lock.json .swcrc ./
|
||||
RUN npm install
|
||||
# syntax=docker/dockerfile:1
|
||||
|
||||
FROM node:16.18.0-alpine3.16 as rest
|
||||
COPY --from=deps /app /app
|
||||
WORKDIR /app
|
||||
COPY src/analytics.ts src/configs.ts src/
|
||||
COPY src/rest src/rest
|
||||
RUN npm run build
|
||||
ARG NODE_VERSION=20
|
||||
|
||||
################################################################################
|
||||
|
||||
FROM node:${NODE_VERSION}-alpine as base
|
||||
|
||||
# Set working directory for all build stages.
|
||||
WORKDIR /usr/src/app
|
||||
|
||||
# Enable corepack so it can install the needed yarn version
|
||||
RUN corepack enable
|
||||
|
||||
COPY package.json .
|
||||
COPY yarn.lock .
|
||||
# We need to copy the yarnrc or else yarn will attempt to use PnP
|
||||
COPY .yarnrc.yml .
|
||||
|
||||
RUN yarn install
|
||||
|
||||
# Copy the rest of the source files into the image.
|
||||
COPY . .
|
||||
|
||||
# Run the build script.
|
||||
RUN yarn build
|
||||
|
||||
# Use production node environment by default.
|
||||
ENV NODE_ENV production
|
||||
|
||||
# Run the application as a non-root user.
|
||||
USER node
|
||||
|
||||
################################################################################
|
||||
|
||||
FROM base as rest
|
||||
|
||||
# Expose the port that the application listens on.
|
||||
EXPOSE 8000
|
||||
CMD ["npm","run","startr"]
|
||||
|
||||
FROM node:16.18.0-alpine3.16 as gateway
|
||||
COPY --from=deps /app /app
|
||||
WORKDIR /app
|
||||
COPY src/configs.ts src/
|
||||
COPY src/gateway src/gateway
|
||||
RUN npm run build
|
||||
# Run the application.
|
||||
CMD node dist/rest/index.js
|
||||
|
||||
################################################################################
|
||||
|
||||
FROM base as gateway
|
||||
|
||||
# Expose the port that the application listens on.
|
||||
EXPOSE 8080
|
||||
CMD ["npm","run","startg"]
|
||||
|
||||
FROM node:16.18.0-alpine3.16 as bot
|
||||
COPY --from=deps /app /app
|
||||
WORKDIR /app
|
||||
COPY prisma prisma
|
||||
COPY node_modules/.prisma/client node_modules/.prisma/client
|
||||
COPY src/analytics.ts src/prisma.ts src/configs.ts src/
|
||||
COPY src/bot src/bot
|
||||
RUN npm run build
|
||||
EXPOSE 8080
|
||||
CMD ["npm","run","startb"]
|
||||
# Run the application.
|
||||
CMD node dist/gateway/index.js
|
||||
|
||||
FROM rabbitmq:3.11.2-management-alpine as rabbitmq
|
||||
COPY src/rabbitmq/plugins plugins
|
||||
################################################################################
|
||||
|
||||
FROM base as bot
|
||||
|
||||
# Expose the port that the application listens on.
|
||||
EXPOSE 8081
|
||||
|
||||
# Run the application.
|
||||
CMD node dist/bot/index.js
|
||||
|
||||
################################################################################
|
||||
|
||||
FROM rabbitmq:3.12-management-alpine as rabbitmq
|
||||
|
||||
# Copy the rabbitmq plugins
|
||||
COPY rabbitmq/plugins/** plugins
|
||||
|
||||
HEALTHCHECK CMD [ "rabbitmq-diagnostics", "-q", "status" ] \
|
||||
--interval=30s --timeout=30s --start-period=30s --retries=5
|
||||
|
||||
# Enable the required plugins
|
||||
RUN rabbitmq-plugins enable rabbitmq_message_deduplication
|
||||
|
||||
@@ -1,66 +1,105 @@
|
||||
# Discordeno Big Bot Template
|
||||
# Big Bot Template
|
||||
|
||||
Support: <https://discord.gg/ddeno>
|
||||
> [!TIP]
|
||||
> If you have any issue you can join the discord server for support: https://discord.gg/ddeno
|
||||
|
||||
This template is designed for bots that aim or are already in millions of Discord servers. It is written with Node.js as
|
||||
currently Deno & Bun are not ready to run something at such a scale. The general idea of this template can be modified
|
||||
for any other runtime if this improves in the future.
|
||||
|
||||
Make sure to install the latest version when you use it.
|
||||
This template is designed for bots that aim to be in millions of Discord services or already are.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Run a find all for `// SETUP-DD-TEMP:` and follow all instructions and delete the comments as you finish them.
|
||||
- Download the source
|
||||
- Copy the .env.example file and rename it to .env
|
||||
- Fill out the .env file
|
||||
- Find all the `TEMPLATE-SETUP:` comments and follow the instructions and delete the comments as you finish them.
|
||||
|
||||
## Startup
|
||||
|
||||
There are two ways to start your bot, using docker and node. Using docker will be the simplest and easiest way to start
|
||||
your bot. The default configuation will be set for Docker.
|
||||
You can run the template using either Docker or Node.
|
||||
|
||||
Using docker will be the simplest and easiest way to start your bot.
|
||||
|
||||
### Using Docker
|
||||
|
||||
The docker compose file include the discordeno bot and influxdb, this would create an enviroment more close to the
|
||||
production enviroment.
|
||||
The docker compose file includes the discordeno bot and influxdb. This will create an environment close to the production environment.
|
||||
|
||||
First, rename the .env.example file to .env, and set the discord token and your dev guild id, change the `REST_URL` to
|
||||
`rest` and `EVENT_HANDLER_URL` to `bot`, set `MESSAGEQUEUE_ENABLE` to true to use message queue, copy the value of
|
||||
`DOCKER_INFLUXDB_INIT_xxxx` to `INFLUX_xxxx`
|
||||
First, copy the `.env.example` file, rename it to `.env`, and fill in the values. Pre-set values can be left to their default value, except for the following:
|
||||
|
||||
Then, run ... to build/rebuild the bot
|
||||
> [!IMPORTANT]
|
||||
> The following values must be set to enable the docker container to communicate between different parts of your bot, InfluxDB, and RabbitMQ
|
||||
>
|
||||
> - `EVENT_HANDLER_HOST` should be set to `bot`
|
||||
> - `REST_HOST` should be set to `rest`
|
||||
> - `GATEWAY_HOST` should be set to `gateway`
|
||||
> - Setup the message queue:
|
||||
> - `MESSAGEQUEUE_ENABLE` should be set to `true`
|
||||
> - `MESSAGEQUEUE_URL` should be set to `rabbitmq:5672`
|
||||
> - `MESSAGEQUEUE_USERNAME` should be set to `guest`
|
||||
> - `MESSAGEQUEUE_PASSWORD` should be set to `guest`
|
||||
> - Set the value for influxDB:
|
||||
> - Copy `DOCKER_INFLUXDB_INIT_ORG` to `INFLUX_ORG`
|
||||
> - Copy `DOCKER_INFLUXDB_INIT_BUCKET` to `INFLUX_BUCKET`
|
||||
> - Copy `DOCKER_INFLUXDB_INIT_ADMIN_TOKEN` to `INFLUX_TOKEN`
|
||||
> - Set `INFLUX_URL` to `http://influxdb:8086`
|
||||
|
||||
- `docker-compose build`
|
||||
After setting the aforementioned values, run `docker compose build` to build/rebuild the bot
|
||||
|
||||
And, run ... to start
|
||||
Finally, run `docker compose up -d` to start
|
||||
|
||||
- `docker-compose up -d`
|
||||
> [!NOTE]
|
||||
> Docker will start the REST proxy, Gateway and Bot, however you won't see any command in Discord.
|
||||
> You will need to manually run the `bot/register-commands.js` file.
|
||||
>
|
||||
> You can do this locally, but you will need to change some environment variables like the `REST_HOST` to point to something accessible from your machine
|
||||
|
||||
Your bot should be running now, you can check the rest/bot process fetch analytics (methods, status...) in influxdb's
|
||||
webgui - <http://localhost:8086> with the username and password in the .env file, message queue's information (number of
|
||||
events...) at <http://localhost:15672> with user: guest and pass: guest.
|
||||
Your bot should be running now.
|
||||
|
||||
You can check the REST process fetch analytics (methods, status...) in influxdb's WebUI at http://localhost:8086 with the username and password in the .env file (`DOCKER_INFLUXDB_INIT_USERNAME` and `DOCKER_INFLUXDB_INIT_PASSWORD`, respectively). You can also check the message queue's information (number of events, ...) in the RabbitMQ WebUI at http://localhost:15672 with the username `guest` and password `guest`.
|
||||
|
||||
### Using Node
|
||||
|
||||
you will need to start a few processes. The instructions below will use `node` but you can use something like `pm2` to
|
||||
help keep your processes alive.
|
||||
> [!NOTE]
|
||||
> This template has been tested with the following versions:
|
||||
>
|
||||
> - NodeJS: v18.20.3, v20.14.0 and v22.2.0
|
||||
> - Any NodeJS version between v18.20.3 and v22.2.0 should work, anything below v18 will not run correctly, anything above v22 should work
|
||||
> - RabbitMQ: v3.12.14 with:
|
||||
> - Erlang: v26.2.5
|
||||
> - [RabbitMQ Message Deduplication Plugin](https://github.com/noxdafox/rabbitmq-message-deduplication): v0.6.2
|
||||
> - InfluxDB: v2.7.6
|
||||
|
||||
First, rename the .env.example file to .env, and set the discord token and your dev guild id, change the `REST_URL` and
|
||||
`EVENT_HANDLER_URL` to `localhost`
|
||||
You will need to start a few processes.
|
||||
|
||||
Then compile everything with `npm run build`.
|
||||
The preset value of `EVENT_HANDLER_HOST`, `REST_HOST`, and `GATEWAY_HOST` all use localhost. If you are using different servers you will need to change those values
|
||||
|
||||
After that, you can start your bot one by one with the following order.
|
||||
#### Setup process
|
||||
|
||||
- Start REST
|
||||
- `npm run startr`
|
||||
- Start Bot
|
||||
- `npm run startb`
|
||||
- Start Gateway
|
||||
- `npm run startg`
|
||||
- Install the dependencies with yarn
|
||||
- Build the code with `yarn build`
|
||||
|
||||
Other things you can add:
|
||||
You can start different parts of your bot in the following order.
|
||||
|
||||
- InfluxDB for logging fetch analytics, by change value of `INFLUX_xxxx` to your influxdb config, leave it empty will
|
||||
disable it.
|
||||
- RabbitMQ for using message queues instead of fetch calls, by change value of `MESSAGEQUEUE_ENABLE` to true, and
|
||||
`MESSAGEQUEUE_xxx` of your rabbitmq config <br/> Note: the RabbitMQ must installed the
|
||||
[RabbitMQ Message Deduplication Plugin](https://github.com/noxdafox/rabbitmq-message-deduplication)
|
||||
- Start the REST Proxy: `yarn start:rest`
|
||||
- Deploy the commands: `node dist/bot/register-commands.js`
|
||||
- Start the Bot: `yarn start:bot`
|
||||
- Start Gateway: `yarn start:gateway`
|
||||
|
||||
#### InfluxDB
|
||||
|
||||
To enable InfluxDB you will need to set the `INFLUX_ORG`, `INFLUX_BUCKET`, `INFLUX_TOKEN`, and `INFLUX_URL` variables in the `.env`. file
|
||||
|
||||
For `INFLUX_URL`, the preset value uses localhost. If your InfluxDB is not running on the same machine, change the URL accordingly (do not include any protocol prefix, just `HOST:PORT`)
|
||||
|
||||
If you do not set one of the values mentioned above, InfluxDB will be disabled.
|
||||
|
||||
#### RabbitMQ
|
||||
|
||||
To enable RabbitMQ you will need to set `MESSAGEQUEUE_ENABLE` to `true` and set `MESSAGEQUEUE_URL`, `MESSAGEQUEUE_USERNAME`, and `MESSAGEQUEUE_PASSWORD` variables in the `.env` file.
|
||||
|
||||
`MESSAGEQUEUE_USERNAME` and `MESSAGEQUEUE_PASSWORD` will both default to `guest` in a RabbitMQ instance unless changed.
|
||||
|
||||
> [!IMPORTANT]
|
||||
> The [RabbitMQ Message Deduplication Plugin](https://github.com/noxdafox/rabbitmq-message-deduplication) must be installed
|
||||
>
|
||||
> The plugin files (`.ez`) are in the `rabbitmq/plugins/message-deduplication` folder. You can copy those into your `plugins` folder for the RabbitMQ installation folder or download them from the original repo (make sure to download the correct version).
|
||||
>
|
||||
> To enable the plugin you will need to run: `rabbitmq-plugins enable rabbitmq_message_deduplication`
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
version: '3.8'
|
||||
services:
|
||||
influxdb:
|
||||
image: 'influxdb:2.4.0-alpine'
|
||||
image: influxdb:2.7.6-alpine
|
||||
ports:
|
||||
- 127.0.0.1:8086:8086
|
||||
env_file:
|
||||
- .env
|
||||
healthcheck:
|
||||
test: "curl -f http://localhost:8086/ping"
|
||||
interval: 5s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
rabbitmq:
|
||||
build:
|
||||
context: .
|
||||
target: rabbitmq
|
||||
ports:
|
||||
- 127.0.0.1:15672:15672
|
||||
|
||||
rest:
|
||||
build:
|
||||
context: .
|
||||
target: rest
|
||||
deploy:
|
||||
replicas: 2
|
||||
depends_on:
|
||||
- influxdb
|
||||
influxdb:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
gateway:
|
||||
@@ -27,9 +31,10 @@ services:
|
||||
context: .
|
||||
target: gateway
|
||||
depends_on:
|
||||
- rest
|
||||
deploy:
|
||||
replicas: 2
|
||||
rest:
|
||||
condition: service_started
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
bot:
|
||||
@@ -37,8 +42,9 @@ services:
|
||||
context: .
|
||||
target: bot
|
||||
depends_on:
|
||||
- rest
|
||||
deploy:
|
||||
replicas: 2
|
||||
rest:
|
||||
condition: service_started
|
||||
rabbitmq:
|
||||
condition: service_healthy
|
||||
env_file:
|
||||
- .env
|
||||
@@ -1,6 +0,0 @@
|
||||
{
|
||||
"watch": "./src/**/*.ts",
|
||||
"ext ": "env,ts",
|
||||
"signal": "SIGKILL",
|
||||
"exec": "npm run build && node"
|
||||
}
|
||||
@@ -1,51 +1,36 @@
|
||||
{
|
||||
"name": "dd-big-bot",
|
||||
"name": "dd-bigbot",
|
||||
"version": "1.0.0",
|
||||
"main": "index.js",
|
||||
"description": "A scalable bot for big bot developers.",
|
||||
"main": "dist/index.js",
|
||||
"type": "module",
|
||||
"license": "ISC",
|
||||
"private": true,
|
||||
"packageManager": "yarn@4.0.2",
|
||||
"scripts": {
|
||||
"devbg": "npx prisma generate && tsc --watch",
|
||||
"fmt": "eslint --fix \"src/**/*.ts*\"",
|
||||
"devg": "nodemon --ignore ./src/bot/**/* --ignore ./src/rest/**/* --ignore ./dist/**/* -e ts dist/gateway/index.js",
|
||||
"devr": "nodemon --ignore ./src/bot/**/* --ignore ./src/gateway/**/* --ignore ./dist/**/* -e ts dist/rest/index.js",
|
||||
"devb": "nodemon --ignore ./src/rest/**/* --ignore ./src/gateway/**/* --ignore ./dist/**/* -e ts dist/bot/index.js",
|
||||
"type": "tsc --noEmit",
|
||||
"build": "swc --delete-dir-on-start src --out-dir dist",
|
||||
"startr": "node dist/rest/index.js",
|
||||
"startg": "node dist/gateway/index.js",
|
||||
"startb": "node dist/bot/index.js"
|
||||
"start:bot": "node dist/bot/index.js",
|
||||
"start:rest": "node dist/rest/index.js",
|
||||
"start:gateway": "node dist/gateway/index.js",
|
||||
"build": "swc src --strip-leading-paths --delete-dir-on-start --out-dir dist",
|
||||
"build:watch": "swc src --strip-leading-paths --delete-dir-on-start --watch --out-dir dist",
|
||||
"dev:bot": "node --watch --watch-preserve-output dist/bot/index.js",
|
||||
"dev:rest": "node --watch --watch-preserve-output dist/rest/index.js",
|
||||
"dev:gateway": "node --watch --watch-preserve-output dist/gateway/index.js",
|
||||
"setup-dd": ""
|
||||
},
|
||||
"dependencies": {
|
||||
"@influxdata/influxdb-client": "^1.29.0",
|
||||
"@prisma/client": "^3.15.2",
|
||||
"amqplib": "^0.10.3",
|
||||
"colorette": "^2.0.19",
|
||||
"discordeno": "^16.0.1",
|
||||
"dotenv": "^16.0.3",
|
||||
"express": "^4.18.1",
|
||||
"fastify": "^4.10.2",
|
||||
"nanoid": "^4.0.0",
|
||||
"node-fetch": "^3.2.10",
|
||||
"web-worker": "^1.2.0"
|
||||
"@discordeno/bot": "19.0.0-next.92bf166",
|
||||
"@fastify/multipart": "^8.3.0",
|
||||
"@influxdata/influxdb-client": "^1.33.2",
|
||||
"amqplib": "^0.10.4",
|
||||
"chalk": "^5.3.0",
|
||||
"fastify": "^4.28.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.1.57",
|
||||
"@swc/core": "^1.3.9",
|
||||
"@types/amqplib": "^0.8.2",
|
||||
"@types/express": "^4.17.13",
|
||||
"@types/node": "^17.0.23",
|
||||
"@types/ws": "^8.5.3",
|
||||
"nodemon": "^2.0.15",
|
||||
"prettier": "2.6.2",
|
||||
"prisma": "^4.2.1",
|
||||
"typescript": "^4.6.3"
|
||||
},
|
||||
"prettier": {
|
||||
"trailingComma": "all",
|
||||
"useTabs": true,
|
||||
"tabWidth": 2,
|
||||
"singleQuote": true,
|
||||
"semi": true,
|
||||
"printWidth": 120
|
||||
"@swc/cli": "^0.3.12",
|
||||
"@swc/core": "^1.6.3",
|
||||
"@types/amqplib": "^0.10.5",
|
||||
"@types/node": "^20.14.6",
|
||||
"typescript": "^5.5.2"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,25 +0,0 @@
|
||||
// This is your Prisma schema file,
|
||||
// learn more about it in the docs: https://pris.ly/d/prisma-schema
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("DATABASE_URL")
|
||||
}
|
||||
|
||||
model Guilds {
|
||||
/// The server id
|
||||
id BigInt @unique @id
|
||||
/// The language the server uses.
|
||||
language String @default("english")
|
||||
}
|
||||
|
||||
model Commands {
|
||||
/// The server id
|
||||
id BigInt @unique @id
|
||||
/// The version number this server is using for it's commands.
|
||||
version Int
|
||||
}
|
||||
18
examples/bigbot/rabbitmq/README.md
Normal file
18
examples/bigbot/rabbitmq/README.md
Normal file
@@ -0,0 +1,18 @@
|
||||
# RabbitMQ
|
||||
|
||||
This template has been tested using RabbitMQ v3.12.14 (Erlang v26.2.5)
|
||||
|
||||
## Plugins
|
||||
|
||||
The template needs the [rabbitmq_message_deduplication](https://github.com/noxdafox/rabbitmq-message-deduplication) plugin.
|
||||
|
||||
This template has been tested with version v0.6.2 of said plugin. The `.ez` files are already in the `message-deduplication` folder, but you can re-download them from the original repository if needed.
|
||||
|
||||
To enable the plugin you will need to run the following command:
|
||||
|
||||
```sh
|
||||
rabbitmq-plugins enable rabbitmq_message_deduplication
|
||||
```
|
||||
|
||||
> [!NOTE]
|
||||
> You may need to prefix the command with `sudo` based on your RabbitMQ setup/current shell permissions
|
||||
2
examples/bigbot/rabbitmq/plugins/.gitattributes
vendored
Normal file
2
examples/bigbot/rabbitmq/plugins/.gitattributes
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
# RabbitMQ plugins are stored in a zip format (.ez files), just for security we tell git to always threat them as binary files
|
||||
* binary
|
||||
Binary file not shown.
Binary file not shown.
@@ -1,55 +0,0 @@
|
||||
import { InfluxDB, Point } from '@influxdata/influxdb-client';
|
||||
import type { RestManager } from 'discordeno/rest';
|
||||
|
||||
const INFLUX_ORG = process.env.INFLUX_ORG as string;
|
||||
const INFLUX_BUCKET = process.env.INFLUX_BUCKET as string;
|
||||
const INFLUX_TOKEN = process.env.INFLUX_TOKEN as string;
|
||||
const INFLUX_URL = process.env.INFLUX_URL as string;
|
||||
|
||||
export const influxDB = INFLUX_URL && INFLUX_TOKEN ? new InfluxDB({ url: INFLUX_URL, token: INFLUX_TOKEN }) : undefined;
|
||||
export const Influx = influxDB?.getWriteApi(INFLUX_ORG, INFLUX_BUCKET);
|
||||
|
||||
export const setupAnalyticsHooks = (rest: RestManager) => {
|
||||
// If influxdb data is provided, enable analytics in this proxy.
|
||||
if (Influx) {
|
||||
rest.fetching = function (options) {
|
||||
Influx?.writePoint(
|
||||
new Point('restEvents')
|
||||
// MARK THE TIME WHEN EVENT ARRIVED
|
||||
.timestamp(new Date())
|
||||
// SET THE GUILD ID
|
||||
.stringField('type', 'REQUEST_FETCHING')
|
||||
.tag('method', options.method)
|
||||
.tag('url', options.url)
|
||||
.tag('bucket', options.bucketId ?? 'NA'),
|
||||
);
|
||||
};
|
||||
|
||||
rest.fetched = function (options, response) {
|
||||
Influx?.writePoint(
|
||||
new Point('restEvents')
|
||||
// MARK THE TIME WHEN EVENT ARRIVED
|
||||
.timestamp(new Date())
|
||||
// SET THE GUILD ID
|
||||
.stringField('type', 'REQUEST_FETCHED')
|
||||
.tag('method', options.method)
|
||||
.tag('url', options.url)
|
||||
.tag('bucket', options.bucketId ?? 'NA')
|
||||
.intField('status', response.status)
|
||||
.tag('statusText', response.statusText),
|
||||
);
|
||||
};
|
||||
|
||||
setInterval(() => {
|
||||
console.log(`[Influx - REST] Saving events...`);
|
||||
Influx?.flush()
|
||||
.then(() => {
|
||||
console.log(`[Influx - REST] Saved events!`);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.log(`[Influx - REST] Error saving events!`, error);
|
||||
});
|
||||
// Every 30seconds
|
||||
}, 30000);
|
||||
}
|
||||
};
|
||||
@@ -1,8 +0,0 @@
|
||||
# Bot Code
|
||||
|
||||
This folder will contain the code for our bot. When events are received from the gateway, they will be handled here.
|
||||
|
||||
## Further Steps
|
||||
|
||||
- Express framework to create the listener however, you can replace it with anything you like. Express is quite a
|
||||
bloated framework. Feel free to optimize to a better framework.
|
||||
@@ -1,63 +1,101 @@
|
||||
import type { Bot } from 'discordeno';
|
||||
import { Collection, createBot, createRestManager } from 'discordeno';
|
||||
import enableHelpersPlugin from 'discordeno/helpers-plugin';
|
||||
import { createLogger } from 'discordeno/logger';
|
||||
import { setupAnalyticsHooks } from '../analytics.js';
|
||||
import { INTENTS, REST_URL } from '../configs.js';
|
||||
import { setupEventHandlers } from './events/mod.js';
|
||||
import type { MessageCollector } from './utils/collectors.js';
|
||||
import { customizeInternals } from './utils/internals/mod.js';
|
||||
import { Collection, LogDepth, createBot, type Bot, type logger } from '@discordeno/bot'
|
||||
import { DISCORD_TOKEN, GATEWAY_AUTHORIZATION, GATEWAY_INTENTS, GATEWAY_URL, REST_AUTHORIZATION, REST_URL } from '../config.js'
|
||||
import type { ManagerGetShardInfoFromGuildId, ShardInfo, WorkerPresencesUpdate, WorkerShardPayload } from '../gateway/worker/types.js'
|
||||
import type { Command } from './commands.js'
|
||||
|
||||
const DISCORD_TOKEN = process.env.DISCORD_TOKEN as string;
|
||||
const REST_AUTHORIZATION = process.env.REST_AUTHORIZATION as string;
|
||||
|
||||
export const bot = enableHelpersPlugin(
|
||||
customizeBot(
|
||||
export const bot = createCustomBot(
|
||||
createBot({
|
||||
token: DISCORD_TOKEN,
|
||||
intents: INTENTS,
|
||||
intents: GATEWAY_INTENTS,
|
||||
rest: {
|
||||
token: DISCORD_TOKEN,
|
||||
proxy: {
|
||||
baseUrl: REST_URL,
|
||||
authorization: REST_AUTHORIZATION,
|
||||
},
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
)
|
||||
|
||||
/** Add custom props to your `bot` here */
|
||||
// SETUP-DD-TEMP: If you want to add any custom props to `bot` you can do so here. Please make sure to also add them in the type below. As an example, i have added a `logger` property. You can add any useful methods or props you wish to have easily available.
|
||||
function customizeBot<B extends Bot = Bot>(bot: B): BotWithCustomProps {
|
||||
const customized = bot as unknown as BotWithCustomProps;
|
||||
customized.logger = createLogger({ name: '[Bot]' });
|
||||
customized.collectors = {
|
||||
messages: new Collection(),
|
||||
};
|
||||
customized.commandVersions = new Collection();
|
||||
overrideGatewayImplementations(bot)
|
||||
|
||||
return customized;
|
||||
// TEMPLATE-SETUP: Add/Remove the desired proprieties that you don't need
|
||||
const props = bot.transformers.desiredProperties
|
||||
|
||||
props.interaction.id = true
|
||||
props.interaction.data = true
|
||||
props.interaction.type = true
|
||||
props.interaction.user = true
|
||||
props.interaction.token = true
|
||||
props.interaction.guildId = true
|
||||
|
||||
props.user.id = true
|
||||
props.user.username = true
|
||||
|
||||
// TEMPLATE-SETUP: If you want/need to add any custom proprieties on the Bot type, you can do it in this function and the `CustomBot` type below. Make sure to do it in both or else you will get an error by TypeScript
|
||||
function createCustomBot<TBot extends Bot = Bot>(rawBot: TBot): CustomBot<TBot> {
|
||||
const bot = rawBot as CustomBot<TBot>
|
||||
|
||||
// We need to set the log depth for the default discordeno logger or else only the first param will be logged
|
||||
;(bot.logger as typeof logger).setDepth(LogDepth.Full)
|
||||
|
||||
bot.commands = new Collection()
|
||||
|
||||
return bot
|
||||
}
|
||||
|
||||
// SETUP-DD-TEMP: If you want to add any custom props to `bot` you can do so here. Please make sure to also add them in the function above. Run a find all and change this to your Bot's name. For example, if your bot's name is Gamer change BotWithCustomProps to Gamer. This way whenever you need to provide the type for the Bot with your custom props it is your bots name.
|
||||
// Note: ALWAYS edit the function above first before adding the type here.
|
||||
export type BotWithCustomProps<B extends Bot = Bot> = B & {
|
||||
/** A easy to use logger to make clean log messages. */
|
||||
logger: ReturnType<typeof createLogger>;
|
||||
/** Collectors that can be used to get input from users. */
|
||||
collectors: {
|
||||
/** Holds the pending messages collectors that users can respond to. */
|
||||
messages: Collection<bigint, MessageCollector>;
|
||||
};
|
||||
/** The command versions for each guild id. */
|
||||
commandVersions: Collection<bigint, number>;
|
||||
};
|
||||
export type CustomBot<TBot extends Bot = Bot> = TBot & {
|
||||
commands: Collection<string, Command>
|
||||
}
|
||||
|
||||
// Example of how to customize internal discordeno stuff easily.
|
||||
customizeInternals(bot);
|
||||
// Override the default gateway functions to allow the methods on the gateway object to proxy the requests to the gateway proxy
|
||||
function overrideGatewayImplementations(bot: CustomBot): void {
|
||||
bot.gateway.sendPayload = async (shardId, payload) => {
|
||||
await fetch(GATEWAY_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
type: 'ShardPayload',
|
||||
shardId,
|
||||
payload,
|
||||
} satisfies WorkerShardPayload),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: GATEWAY_AUTHORIZATION,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Setup event handlers.
|
||||
setupEventHandlers();
|
||||
bot.gateway.editBotStatus = async (payload) => {
|
||||
await fetch(GATEWAY_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
type: 'EditShardsPresence',
|
||||
payload,
|
||||
} satisfies WorkerPresencesUpdate),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: GATEWAY_AUTHORIZATION,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
bot.rest = createRestManager({
|
||||
token: DISCORD_TOKEN,
|
||||
secretKey: REST_AUTHORIZATION,
|
||||
customUrl: REST_URL,
|
||||
});
|
||||
export async function getShardInfoFromGuild(guildId?: bigint): Promise<Omit<ShardInfo, 'nonce'>> {
|
||||
const req = await fetch(GATEWAY_URL, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({
|
||||
type: 'ShardInfoFromGuild',
|
||||
guildId: guildId?.toString(),
|
||||
} as ManagerGetShardInfoFromGuildId),
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
Authorization: GATEWAY_AUTHORIZATION,
|
||||
},
|
||||
})
|
||||
|
||||
// Add send fetching analytics hook to rest
|
||||
setupAnalyticsHooks(bot.rest);
|
||||
const res = await req.json()
|
||||
|
||||
if (req.ok) return res
|
||||
|
||||
throw new Error(`There was an issue getting the shard info: ${res.error}`)
|
||||
}
|
||||
|
||||
85
examples/bigbot/src/bot/commands.ts
Normal file
85
examples/bigbot/src/bot/commands.ts
Normal file
@@ -0,0 +1,85 @@
|
||||
import type {
|
||||
ApplicationCommandOptionTypes,
|
||||
Attachment,
|
||||
CamelizedDiscordApplicationCommandOption,
|
||||
ChannelTypes,
|
||||
CreateApplicationCommand,
|
||||
Interaction,
|
||||
Member,
|
||||
Role,
|
||||
User,
|
||||
} from '@discordeno/bot'
|
||||
import { bot } from './bot.js'
|
||||
|
||||
export default function createCommand<const TOptions extends CommandOptions>(command: Command<TOptions>): void {
|
||||
bot.commands.set(command.name, command as Command)
|
||||
}
|
||||
|
||||
export type Command<TOptions extends CommandOptions = CommandOptions> = CreateApplicationCommand & {
|
||||
/** @inheritdoc */
|
||||
options?: TOptions
|
||||
/**
|
||||
* Should this command be only deployed on the Dev guild?
|
||||
*
|
||||
* @default false
|
||||
*/
|
||||
devOnly?: boolean
|
||||
/** Function to run when the interaction is executed */
|
||||
run: (interaction: Interaction, options: GetCommandOptions<TOptions>) => unknown
|
||||
/** Function to run when an autocomplete interaction is fired */
|
||||
autoComplete?: (interaction: Interaction, options: GetCommandOptions<TOptions>) => unknown
|
||||
}
|
||||
|
||||
export type GetCommandOptions<T extends CommandOptions> = T extends CommandOptions
|
||||
? { [Prop in keyof BuildOptions<T> as Prop]: BuildOptions<T>[Prop] }
|
||||
: never
|
||||
|
||||
export type CommandOption = CamelizedDiscordApplicationCommandOption
|
||||
export type CommandOptions = CommandOption[]
|
||||
|
||||
// Option parsing
|
||||
|
||||
interface UserResolved {
|
||||
user: User
|
||||
member: Member | undefined
|
||||
}
|
||||
|
||||
interface ChannelResolved {
|
||||
id: bigint
|
||||
name: string
|
||||
type: ChannelTypes
|
||||
permissions: bigint
|
||||
}
|
||||
|
||||
type ResolvedValues = number | boolean | UserResolved | Role | ChannelResolved | Attachment
|
||||
|
||||
/**
|
||||
* From here SubCommandGroup and SubCommand are missing, this is wanted.
|
||||
*
|
||||
* The entries are sorted based on the enum value
|
||||
*/
|
||||
interface TypeToResolvedMap {
|
||||
[ApplicationCommandOptionTypes.String]: string
|
||||
[ApplicationCommandOptionTypes.Integer]: number
|
||||
[ApplicationCommandOptionTypes.Boolean]: boolean
|
||||
[ApplicationCommandOptionTypes.User]: UserResolved
|
||||
[ApplicationCommandOptionTypes.Channel]: ChannelResolved
|
||||
[ApplicationCommandOptionTypes.Role]: Role
|
||||
[ApplicationCommandOptionTypes.Mentionable]: Role | UserResolved
|
||||
[ApplicationCommandOptionTypes.Number]: number
|
||||
[ApplicationCommandOptionTypes.Attachment]: Attachment
|
||||
}
|
||||
|
||||
type ConvertTypeToResolved<T extends ApplicationCommandOptionTypes> = T extends keyof TypeToResolvedMap ? TypeToResolvedMap[T] : ResolvedValues
|
||||
|
||||
type SubCommandApplicationCommand = ApplicationCommandOptionTypes.SubCommand | ApplicationCommandOptionTypes.SubCommandGroup
|
||||
type GetOptionName<T> = T extends { name: string } ? T['name'] : never
|
||||
type GetOptionValue<T> = T extends { type: ApplicationCommandOptionTypes; required?: boolean }
|
||||
? T extends { type: SubCommandApplicationCommand; options?: CommandOptions }
|
||||
? BuildOptions<T['options']>
|
||||
: ConvertTypeToResolved<T['type']> | (T['required'] extends true ? never : undefined)
|
||||
: never
|
||||
|
||||
type BuildOptions<T extends CommandOptions | undefined> = {
|
||||
[Prop in keyof Omit<T, keyof unknown[]> as GetOptionName<T[Prop]>]: GetOptionValue<T[Prop]>
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import { ApplicationCommandOptionTypes } from 'discordeno';
|
||||
import { prisma } from '../../prisma.js';
|
||||
import languages from '../languages/languages.js';
|
||||
import { serverLanguages, translate } from '../languages/translate.js';
|
||||
import { createCommand } from '../utils/slash/createCommand.js';
|
||||
|
||||
export default createCommand({
|
||||
name: 'LANGUAGE_NAME',
|
||||
description: 'LANGUAGE_DESCRIPTION',
|
||||
options: [
|
||||
{
|
||||
name: 'LANGUAGE_KEY_NAME',
|
||||
description: 'LANGUAGE_KEY_DESCRIPTION',
|
||||
type: ApplicationCommandOptionTypes.String,
|
||||
choices: Object.keys(languages).map((key) => ({ name: key, value: key })),
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
execute: async function (_, interaction, args) {
|
||||
if (!interaction.guildId) return;
|
||||
|
||||
// Set the new language in cache
|
||||
serverLanguages.set(interaction.guildId, args.name);
|
||||
// Let the user know its been updated.
|
||||
await interaction.reply(translate(interaction.guildId!, 'LANGUAGE_UPDATED', args.name));
|
||||
// Update the db
|
||||
return await prisma.guilds.upsert({
|
||||
where: { id: interaction.guildId },
|
||||
create: { language: args.name, id: interaction.guildId },
|
||||
update: { language: args.name },
|
||||
});
|
||||
},
|
||||
});
|
||||
@@ -1,9 +0,0 @@
|
||||
import language from './language.js';
|
||||
import ping from './ping.js';
|
||||
|
||||
export const COMMANDS = {
|
||||
language,
|
||||
ping,
|
||||
};
|
||||
|
||||
export default COMMANDS;
|
||||
@@ -1,17 +1,16 @@
|
||||
import { translate } from '../languages/translate.js';
|
||||
import { createCommand } from '../utils/slash/createCommand.js';
|
||||
import { snowflakeToTimestamp } from '@discordeno/bot'
|
||||
import { getShardInfoFromGuild } from '../bot.js'
|
||||
import createCommand from '../commands.js'
|
||||
|
||||
export default createCommand({
|
||||
name: 'PING_NAME',
|
||||
description: 'PING_DESCRIPTION',
|
||||
execute: async function (_, interaction) {
|
||||
return await interaction.reply(
|
||||
translate(interaction.guildId!, 'PING_RESPONSE_WITH_TIME', Date.now() - snowflakeToTimestamp(interaction.id)),
|
||||
);
|
||||
createCommand({
|
||||
name: 'ping',
|
||||
description: '🏓 Check whether the bot is online and responsive.',
|
||||
async run(interaction) {
|
||||
const ping = Date.now() - snowflakeToTimestamp(interaction.id)
|
||||
const shardInfo = await getShardInfoFromGuild(interaction.guildId)
|
||||
|
||||
const shardPing = shardInfo.rtt === -1 ? '*Not yet available*' : `${shardInfo.rtt}ms`
|
||||
|
||||
await interaction.respond(`🏓 Pong! Gateway Latency: ${shardPing}, Roundtrip Latency: ${ping}ms. I am online and responsive! 🕙`)
|
||||
},
|
||||
});
|
||||
|
||||
// TODO: This should be deleted once this is available in the helpers plugin.
|
||||
export function snowflakeToTimestamp(id: bigint) {
|
||||
return Number(id / 4194304n + 1420070400000n);
|
||||
}
|
||||
})
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
import type { Interaction } from 'discordeno';
|
||||
import type { BotWithCustomProps } from '../../bot.js';
|
||||
|
||||
export async function executeButtonClick(bot: BotWithCustomProps, interaction: Interaction) {
|
||||
if (!interaction.data) return;
|
||||
|
||||
bot.logger.info(
|
||||
`[Button] The ${interaction.data.customId} button was clicked in Guild: ${interaction.guildId} by ${interaction.user.id}.`,
|
||||
);
|
||||
|
||||
await Promise.allSettled([
|
||||
// SETUP-DD-TEMP: Insert any functions you wish to run when a user clicks a button.
|
||||
]).catch(console.log);
|
||||
}
|
||||
@@ -1,304 +0,0 @@
|
||||
import { bgBlack, bgGreen, bgMagenta, bgYellow, black, green, red, white } from 'colorette';
|
||||
import type {
|
||||
ApplicationCommandOption,
|
||||
Bot,
|
||||
Channel,
|
||||
ChannelTypes,
|
||||
Interaction,
|
||||
InteractionDataOption,
|
||||
Member,
|
||||
Role,
|
||||
User,
|
||||
} from 'discordeno';
|
||||
import { ApplicationCommandOptionTypes, InteractionResponseTypes } from 'discordeno';
|
||||
import type { BotWithCustomProps } from '../../bot.js';
|
||||
import { bot } from '../../bot.js';
|
||||
import COMMANDS from '../../commands/mod.js';
|
||||
import type { translationKeys } from '../../languages/translate.js';
|
||||
import { getLanguage, loadLanguage, serverLanguages, translate } from '../../languages/translate.js';
|
||||
import type { InteractionWithCustomProps } from '../../typings/discordeno.js';
|
||||
import type { Command, ConvertArgumentDefinitionsToArgs } from '../../utils/slash/createCommand.js';
|
||||
|
||||
function logCommand(
|
||||
info: Interaction,
|
||||
type: 'Failure' | 'Success' | 'Trigger' | 'Slowmode' | 'Missing' | 'Inhibit',
|
||||
commandName: string,
|
||||
) {
|
||||
const command = `[COMMAND: ${bgYellow(black(commandName || 'Unknown'))} - ${bgBlack(
|
||||
['Failure', 'Slowmode', 'Missing'].includes(type) ? red(type) : type === 'Success' ? green(type) : white(type),
|
||||
)}]`;
|
||||
|
||||
const user = bgGreen(
|
||||
black(`${info.user.username}#${info.user.discriminator.toString().padStart(4, '0')}(${info.id})`),
|
||||
);
|
||||
const guild = bgMagenta(black(`${info.guildId ? `Guild ID: (${info.guildId})` : 'DM'}`));
|
||||
|
||||
bot.logger.info(`${command} by ${user} in ${guild} with MessageID: ${info.id}`);
|
||||
}
|
||||
|
||||
export async function executeSlashCommand(bot: BotWithCustomProps, interaction: InteractionWithCustomProps) {
|
||||
const data = interaction.data;
|
||||
const name = data?.name as keyof typeof COMMANDS;
|
||||
|
||||
const command: Command<any> | undefined = COMMANDS[name];
|
||||
|
||||
// Command could not be found
|
||||
if (!command?.execute) {
|
||||
return await interaction
|
||||
.reply(translate(interaction.guildId!, 'EXECUTE_COMMAND_NOT_FOUND'))
|
||||
.catch(bot.logger.error);
|
||||
}
|
||||
|
||||
// HAVE TO CONVERT OUTSIDE OF TRY SO IT CAN BE USED IN CATCH TOO
|
||||
try {
|
||||
logCommand(interaction, 'Trigger', name);
|
||||
|
||||
// Check subcommand permissions and options
|
||||
if (!(await commandAllowed(interaction, command))) return;
|
||||
|
||||
// Load the language for this guild
|
||||
if (interaction.guildId && !serverLanguages.has(interaction.guildId)) {
|
||||
// Todo: make command.execute reply change to editReply after running this
|
||||
// await interaction.reply({
|
||||
// type: InteractionResponseTypes.DeferredChannelMessageWithSource,
|
||||
// });
|
||||
await loadLanguage(interaction.guildId);
|
||||
} // Load the language for this guild
|
||||
else if (command.acknowledge) {
|
||||
// Acknowledge the command
|
||||
await interaction.reply({
|
||||
type: InteractionResponseTypes.DeferredChannelMessageWithSource,
|
||||
});
|
||||
}
|
||||
|
||||
// FIRST GET THE TRANSLATIONS FOR ALL OPTIONS
|
||||
const translatedOptionNames =
|
||||
interaction.guildId && command.options ? translateOptionNames(bot, interaction.guildId, command.options) : {};
|
||||
|
||||
// PARSE THE OPTIONS TO A NICE OBJECT AND TRANSLATE THE KEYS TO ENGLISH
|
||||
const parsedArguments = optionParser(interaction, translatedOptionNames);
|
||||
|
||||
await command.execute(bot, interaction, parsedArguments as ConvertArgumentDefinitionsToArgs<any>);
|
||||
logCommand(interaction, 'Success', name);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
logCommand(interaction, 'Failure', name);
|
||||
|
||||
try {
|
||||
console.log('try');
|
||||
// try to reply the interaction, becuase we don't know if it replied or deffered
|
||||
return await interaction.reply(translate(interaction.id, 'EXECUTE_COMMAND_ERROR'));
|
||||
} catch {
|
||||
console.log('catch');
|
||||
// edit the reply or deffered reply of interaction
|
||||
return await interaction.editReply(translate(interaction.id, 'EXECUTE_COMMAND_ERROR')).catch(bot.logger.error);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** Runs the inhibitors to see if a command is allowed to run. */
|
||||
export async function commandAllowed(interaction: InteractionWithCustomProps, command: Command<any>) {
|
||||
// CHECK WHETHER THE USER/GUILD IS VIP
|
||||
if (command.vipOnly) {
|
||||
// SETUP-DD-TEMP: Check if this server/user is a vip.
|
||||
const isVIP = true;
|
||||
|
||||
if (!isVIP) {
|
||||
await interaction.reply(translate(interaction.id, 'NEED_VIP')).catch(bot.logger.error);
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Mapped by `language-commandName`
|
||||
const translatedOptionNamesCache = new Map<string, Record<string, string>>();
|
||||
|
||||
/** Translates all options of the command to an object: translatedOptionName: optionName */
|
||||
export function translateOptionNames(
|
||||
bot: Bot,
|
||||
guildId: bigint,
|
||||
options: ApplicationCommandOption[],
|
||||
commandName?: string,
|
||||
): Record<string, string> {
|
||||
const language = getLanguage(guildId);
|
||||
// RETURN THE ALREADY TRANSLATED OPTIONS WHICH ARE IN CACHE
|
||||
if (commandName && translatedOptionNamesCache.has(`${language}-${commandName}`)) {
|
||||
return translatedOptionNamesCache.get(`${language}-${commandName}`)!;
|
||||
}
|
||||
|
||||
// TRANSLATE ALL OPTIONS
|
||||
let translated: Record<string, string> = {};
|
||||
for (const option of options) {
|
||||
translated[translate(guildId, option.name as translationKeys).toLowerCase()] = translate(
|
||||
'english',
|
||||
option.name as translationKeys,
|
||||
);
|
||||
if (option.options) {
|
||||
translated = {
|
||||
...translated,
|
||||
...translateOptionNames(bot, guildId, option.options),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// SAVE THE TRANSLATED OPTIONS IN CACHE FOR FASTER ACCESS
|
||||
if (commandName) {
|
||||
translatedOptionNamesCache.set(`${language}-${commandName}`, translated);
|
||||
}
|
||||
|
||||
return translated;
|
||||
}
|
||||
|
||||
function convertOptionValue(
|
||||
interaction: Interaction,
|
||||
option: InteractionDataOption,
|
||||
translateOptions?: Record<string, string>,
|
||||
): [
|
||||
string,
|
||||
(
|
||||
| { user: User; member: Member }
|
||||
| Role
|
||||
| {
|
||||
id: bigint;
|
||||
name: string;
|
||||
type: ChannelTypes;
|
||||
permissions: bigint;
|
||||
}
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
),
|
||||
] {
|
||||
// THE OPTION IS A CHANNEL
|
||||
if (option.type === ApplicationCommandOptionTypes.Channel) {
|
||||
const channel = interaction.data?.resolved?.channels?.get(BigInt(option.value as string));
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [translateOptions?.[option.name] ?? option.name, channel];
|
||||
}
|
||||
|
||||
// THE OPTION IS A ROLE
|
||||
if (option.type === ApplicationCommandOptionTypes.Role) {
|
||||
const role = interaction.data?.resolved?.roles?.get(BigInt(option.value as string));
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [translateOptions?.[option.name] ?? option.name, role];
|
||||
}
|
||||
|
||||
// THE OPTION IS A USER
|
||||
if (option.type === ApplicationCommandOptionTypes.User) {
|
||||
const user = interaction.data?.resolved?.users?.get(BigInt(option.value as string));
|
||||
const member = interaction.data?.resolved?.members?.get(BigInt(option.value as string));
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [
|
||||
translateOptions?.[option.name] ?? option.name,
|
||||
{
|
||||
member,
|
||||
user,
|
||||
},
|
||||
];
|
||||
}
|
||||
|
||||
// THE OPTION IS A MENTIONABLE
|
||||
if (option.type === ApplicationCommandOptionTypes.Mentionable) {
|
||||
const role = interaction.data?.resolved?.roles?.get(BigInt(option.value as string));
|
||||
const user = interaction.data?.resolved?.users?.get(BigInt(option.value as string));
|
||||
const member = interaction.data?.resolved?.members?.get(BigInt(option.value as string));
|
||||
|
||||
const final = user && member ? { user, member } : role;
|
||||
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
return [translateOptions?.[option.name] ?? option.name, final];
|
||||
}
|
||||
|
||||
// THE REST OF OPTIONS DON'T NEED ANY CONVERTION
|
||||
// SAVE THE ARGUMENT WITH THE CORRECT NAME
|
||||
// @ts-expect-error
|
||||
return [translateOptions?.[option.name] ?? option.name, option.value];
|
||||
}
|
||||
|
||||
/** Parse the options to a nice object.
|
||||
* NOTE: this does not work with subcommands
|
||||
*/
|
||||
export function optionParser(
|
||||
interaction: Interaction,
|
||||
translateOptions?: Record<string, string>,
|
||||
):
|
||||
| InteractionCommandArgs
|
||||
| { [key: string]: InteractionCommandArgs }
|
||||
| { [key: string]: { [key: string]: InteractionCommandArgs } } {
|
||||
// OPTIONS CAN BE UNDEFINED SO WE JUST RETURN AN EMPTY OBJECT
|
||||
if (!interaction.data?.options) return {};
|
||||
|
||||
// A SUBCOMMAND WAS USED
|
||||
if (interaction.data.options[0]?.type === ApplicationCommandOptionTypes.SubCommand) {
|
||||
const convertedOptions: Record<
|
||||
string,
|
||||
| { user: User; member: Member }
|
||||
| Role
|
||||
| {
|
||||
id: bigint;
|
||||
name: string;
|
||||
type: ChannelTypes;
|
||||
permissions: bigint;
|
||||
}
|
||||
| boolean
|
||||
| string
|
||||
| number
|
||||
> = {};
|
||||
// CONVERT ALL THE OPTIONS
|
||||
for (const option of interaction.data.options[0].options ?? []) {
|
||||
const [name, value] = convertOptionValue(interaction, option, translateOptions);
|
||||
convertedOptions[name] = value;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
return {
|
||||
[translateOptions?.[interaction.data.options[0].name] ?? interaction.data.options[0].name]: convertedOptions,
|
||||
};
|
||||
}
|
||||
|
||||
// A SUBCOMMAND GROUP WAS USED
|
||||
if (interaction.data.options[0]?.type === ApplicationCommandOptionTypes.SubCommandGroup) {
|
||||
const convertedOptions: Record<string, Member | Role | Channel | boolean | string | number> = {};
|
||||
// CONVERT ALL THE OPTIONS
|
||||
for (const option of interaction.data.options[0]?.options![0]?.options ?? []) {
|
||||
const [name, value] = convertOptionValue(interaction, option, translateOptions);
|
||||
// @ts-expect-error
|
||||
convertedOptions[name] = value;
|
||||
}
|
||||
|
||||
// @ts-expect-error
|
||||
return {
|
||||
[translateOptions?.[interaction.data.options[0].name] ?? interaction.data.options[0].name]: {
|
||||
[translateOptions?.[interaction.data.options[0]!.options![0]!.name] ??
|
||||
interaction.data.options[0]!.options![0]!.name]: convertedOptions,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// A NORMAL COMMAND WAS USED
|
||||
const convertedOptions: Record<
|
||||
string,
|
||||
Member | Role | Record<string, Pick<Channel, 'id' | 'name' | 'type' | 'permissions'>> | boolean | string | number
|
||||
> = {};
|
||||
for (const option of interaction.data.options ?? []) {
|
||||
const [name, value] = convertOptionValue(interaction, option, translateOptions);
|
||||
// @ts-expect-error
|
||||
convertedOptions[name] = value;
|
||||
}
|
||||
|
||||
return convertedOptions;
|
||||
}
|
||||
|
||||
/** The interaction arguments.
|
||||
* Important the members `deaf` and `mute` properties will always be false.
|
||||
*/
|
||||
export type InteractionCommandArgs = Record<
|
||||
string,
|
||||
Member | Role | Record<string, Pick<Channel, 'id' | 'name' | 'type' | 'permissions'>> | boolean | string | number
|
||||
>;
|
||||
53
examples/bigbot/src/bot/events/interactions/create.ts
Normal file
53
examples/bigbot/src/bot/events/interactions/create.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { InteractionTypes, LogLevels, commandOptionsParser, type Interaction, type logger } from '@discordeno/bot'
|
||||
import chalk from 'chalk'
|
||||
import { bot } from '../../bot.js'
|
||||
|
||||
bot.events.interactionCreate = async (interaction) => {
|
||||
const isAutocomplete = interaction.type === InteractionTypes.ApplicationCommandAutocomplete
|
||||
const isCommandOrAutocomplete = interaction.type === InteractionTypes.ApplicationCommand || isAutocomplete
|
||||
|
||||
if (!interaction.data || !isCommandOrAutocomplete) return
|
||||
|
||||
const command = bot.commands.get(interaction.data.name)
|
||||
|
||||
if (!command) {
|
||||
logCommand(interaction, 'Missing', interaction.data.name)
|
||||
await interaction.respond('❌ Something went wrong. I was not able to find this command.')
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
logCommand(interaction, 'Trigger', interaction.data.name)
|
||||
|
||||
const options = commandOptionsParser(interaction)
|
||||
|
||||
try {
|
||||
if (isAutocomplete) {
|
||||
await command.autoComplete?.(interaction, options)
|
||||
} else {
|
||||
await command.run(interaction, options)
|
||||
}
|
||||
|
||||
logCommand(interaction, 'Success', interaction.data.name)
|
||||
} catch (error) {
|
||||
logCommand(interaction, 'Failure', interaction.data.name, LogLevels.Error, error)
|
||||
await interaction.respond('❌ Something went wrong. The command execution has thrown an error.')
|
||||
}
|
||||
}
|
||||
|
||||
function logCommand(
|
||||
interaction: Interaction,
|
||||
type: 'Failure' | 'Success' | 'Trigger' | 'Missing',
|
||||
commandName: string,
|
||||
logLevel: LogLevels = LogLevels.Info,
|
||||
...restArgs: unknown[]
|
||||
): void {
|
||||
const typeColor = ['Failure', 'Missing'].includes(type) ? chalk.red(type) : type === 'Success' ? chalk.green(type) : chalk.white(type)
|
||||
|
||||
const autocomplete = interaction.type === InteractionTypes.ApplicationCommandAutocomplete ? ' (AutoComplete) ' : ''
|
||||
const command = `Command${autocomplete}: ${chalk.bgYellow.black(commandName || 'Unknown')} - ${chalk.bgBlack(typeColor)}`
|
||||
const user = chalk.bgGreen.black(`@${interaction.user.username} (${interaction.user.id})`)
|
||||
const guild = chalk.bgMagenta.black(interaction.guildId ? `guildId: ${interaction.guildId}` : 'DM')
|
||||
|
||||
;(bot.logger as typeof logger).log(logLevel, `${command} - By ${user} in ${guild}`, ...restArgs)
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
import { InteractionTypes, MessageComponentTypes } from 'discordeno';
|
||||
import { bot } from '../../bot.js';
|
||||
import type { InteractionWithCustomProps } from '../../typings/discordeno.js';
|
||||
import { executeButtonClick } from './button.js';
|
||||
import { executeSlashCommand } from './command.js';
|
||||
import { executeModalSubmit } from './modal.js';
|
||||
|
||||
export function setInteractionCreateEvent() {
|
||||
bot.events.interactionCreate = async function (_, interaction) {
|
||||
if (interaction.type === InteractionTypes.ApplicationCommand) {
|
||||
await executeSlashCommand(bot, interaction as InteractionWithCustomProps);
|
||||
} else if (interaction.type === InteractionTypes.MessageComponent) {
|
||||
if (!interaction.data) return;
|
||||
|
||||
// THE INTERACTION CAME FROM A BUTTON
|
||||
if (interaction.data.componentType === MessageComponentTypes.Button) {
|
||||
await executeButtonClick(bot, interaction);
|
||||
}
|
||||
} else if (interaction.type === InteractionTypes.ModalSubmit) {
|
||||
await executeModalSubmit(bot, interaction);
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import type { Interaction } from 'discordeno';
|
||||
import type { BotWithCustomProps } from '../../bot.js';
|
||||
|
||||
export async function executeModalSubmit(bot: BotWithCustomProps, interaction: Interaction) {
|
||||
if (!interaction.data) return;
|
||||
|
||||
bot.logger.info(
|
||||
`[Modal] The ${interaction.data?.customId || 'UNKNWON'} modal was submitted in Guild: ${interaction.guildId} by ${
|
||||
interaction.user.id
|
||||
}.`,
|
||||
);
|
||||
|
||||
await Promise.allSettled([
|
||||
// SETUP-DD-TEMP: Insert any functions you wish to run when a user clicks a button.
|
||||
]).catch(console.log);
|
||||
}
|
||||
@@ -1,12 +0,0 @@
|
||||
import { bot } from '../../bot.js';
|
||||
import { processMessageCollectors } from '../../utils/collectors.js';
|
||||
|
||||
export function setMessageCreateEvent() {
|
||||
bot.events.messageCreate = async function (_, message) {
|
||||
processMessageCollectors(message);
|
||||
|
||||
await Promise.allSettled([
|
||||
// SETUP-DD-TEMP: Add any functions you want to run on every message here. For example, automoderation filters.
|
||||
]).catch(console.log);
|
||||
};
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
import { setInteractionCreateEvent } from './interactions/mod.js';
|
||||
import { setMessageCreateEvent } from './messages/create.js';
|
||||
import { setRawEvent } from './raw.js';
|
||||
|
||||
export function setupEventHandlers() {
|
||||
setInteractionCreateEvent();
|
||||
setRawEvent();
|
||||
setMessageCreateEvent();
|
||||
}
|
||||
21
examples/bigbot/src/bot/events/nodejs.ts
Normal file
21
examples/bigbot/src/bot/events/nodejs.ts
Normal file
@@ -0,0 +1,21 @@
|
||||
import { createEmbeds } from '@discordeno/bot'
|
||||
import { inspect } from 'node:util'
|
||||
import { BUGS_ERRORS_REPORT_WEBHOOK } from '../../config.js'
|
||||
import { bot } from '../bot.js'
|
||||
import { webhookURLToIDAndToken } from '../utils/webhook.js'
|
||||
|
||||
process.on('unhandledRejection', async (error) => {
|
||||
bot.logger.error('An unhandled rejection occurred', error)
|
||||
|
||||
if (!BUGS_ERRORS_REPORT_WEBHOOK || !error) return
|
||||
|
||||
const { id, token } = webhookURLToIDAndToken(BUGS_ERRORS_REPORT_WEBHOOK)
|
||||
|
||||
if (!id || !token) return
|
||||
|
||||
const inspectedError = inspect(error)
|
||||
|
||||
const embeds = createEmbeds().setDescription(`\`\`\`${inspectedError}\`\`\``).setFooter('Unhandled rejection occurred').setTimestamp(Date.now())
|
||||
|
||||
await bot.helpers.executeWebhook(id, token, { embeds })
|
||||
})
|
||||
@@ -1,38 +0,0 @@
|
||||
import type { DiscordUnavailableGuild } from 'discordeno';
|
||||
import { prisma } from '../../prisma.js';
|
||||
import { bot } from '../bot.js';
|
||||
import { updateGuildCommands, usesLatestCommandVersion } from '../utils/slash/updateCommands.js';
|
||||
|
||||
/** To prevent updating every guild when a shard goes ready we have to ignore them using this */
|
||||
// export const initialyLoadingGuildIds = new Set<bigint>()
|
||||
|
||||
export function setRawEvent() {
|
||||
bot.events.raw = async function (_, data) {
|
||||
if (data.t === 'GUILD_DELETE') {
|
||||
const id = (data.d as DiscordUnavailableGuild).id;
|
||||
|
||||
return await prisma.commands.delete({ where: { id: bot.transformers.snowflake(id) } });
|
||||
}
|
||||
|
||||
const id = bot.transformers.snowflake(
|
||||
(data.t && ['GUILD_UPDATE', 'GUILD_CREATE'].includes(data.t)
|
||||
? // deno-lint-ignore no-explicit-any
|
||||
data.d?.id
|
||||
: // deno-lint-ignore no-explicit-any
|
||||
data.d?.guild_id) ?? '',
|
||||
);
|
||||
|
||||
// The GUILD_CREATE event came from a shard loaded event so ignore it
|
||||
if (['READY', 'GUILD_LOADED_DD', null].includes(data.t)) return;
|
||||
|
||||
// console.log({ id, v: await usesLatestCommandVersion(id) })
|
||||
|
||||
if (!id || (await usesLatestCommandVersion(id))) return;
|
||||
// dev guild
|
||||
if (id === 547046977578336286n) return;
|
||||
|
||||
// NEW GUILD AVAILABLE
|
||||
bot.logger.info(`[Slash Setup] Installing Slash commands on Guild ${id} event type: ${data.t}`);
|
||||
await updateGuildCommands(bot, id).catch(bot.logger.error);
|
||||
};
|
||||
}
|
||||
17
examples/bigbot/src/bot/fastify.ts
Normal file
17
examples/bigbot/src/bot/fastify.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import fastify, { type FastifyInstance } from 'fastify'
|
||||
import { EVENT_HANDLER_AUTHORIZATION } from '../config.js'
|
||||
|
||||
export function buildFastifyApp(): FastifyInstance {
|
||||
const app = fastify()
|
||||
|
||||
// Authorization check
|
||||
app.addHook('onRequest', async (req, res) => {
|
||||
if (req.headers.authorization !== EVENT_HANDLER_AUTHORIZATION) {
|
||||
res.status(401).send({
|
||||
message: 'Credentials not valid.',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
return app
|
||||
}
|
||||
@@ -1,185 +1,123 @@
|
||||
import dotenv from 'dotenv';
|
||||
import type { DiscordGatewayPayload, GatewayDispatchEventNames } from '@discordeno/bot'
|
||||
import { connect as connectAmqp } from 'amqplib'
|
||||
import { join as joinPath } from 'node:path'
|
||||
import {
|
||||
EVENT_HANDLER_HOST,
|
||||
EVENT_HANDLER_PORT,
|
||||
MESSAGEQUEUE_ENABLE,
|
||||
MESSAGEQUEUE_PASSWORD,
|
||||
MESSAGEQUEUE_URL,
|
||||
MESSAGEQUEUE_USERNAME,
|
||||
} from '../config.js'
|
||||
import { getDirnameFromFileUrl } from '../util.js'
|
||||
import { bot } from './bot.js'
|
||||
import { buildFastifyApp } from './fastify.js'
|
||||
import importDirectory from './utils/loader.js'
|
||||
|
||||
import type { DiscordGatewayPayload } from 'discordeno';
|
||||
// ReferenceError: publishMessage is not defined
|
||||
// import Embeds from "discordeno/embeds";
|
||||
import amqplib from 'amqplib';
|
||||
import express from 'express';
|
||||
import { BOT_ID, EVENT_HANDLER_URL } from '../configs.js';
|
||||
import { bot } from './bot.js';
|
||||
import { updateDevCommands } from './utils/slash/updateCommands.js';
|
||||
import { webhookURLToIDAndToken } from './utils/webhook.js';
|
||||
dotenv.config();
|
||||
// The importDirectory function uses 'readdir' that requires either a relative path compared to the process CWD or an absolute one, so to get one relative we need to use import.meta.url
|
||||
const currentDirectory = getDirnameFromFileUrl(import.meta.url)
|
||||
|
||||
const BUGS_ERRORS_REPORT_WEBHOOK = process.env.BUGS_ERRORS_REPORT_WEBHOOK;
|
||||
const EVENT_HANDLER_AUTHORIZATION = process.env.EVENT_HANDLER_AUTHORIZATION as string;
|
||||
const EVENT_HANDLER_PORT = process.env.EVENT_HANDLER_PORT as string;
|
||||
await importDirectory(joinPath(currentDirectory, './commands'))
|
||||
await importDirectory(joinPath(currentDirectory, './events'))
|
||||
|
||||
process
|
||||
.on('unhandledRejection', (error) => {
|
||||
if (!BUGS_ERRORS_REPORT_WEBHOOK) return;
|
||||
const { id, token } = webhookURLToIDAndToken(BUGS_ERRORS_REPORT_WEBHOOK);
|
||||
if (!id || !token) return;
|
||||
|
||||
// DO NOT SEND ERRORS FROM NON PRODUCTION
|
||||
if (BOT_ID !== 270010330782892032n) {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
// An unhandled error occurred on the bot in production
|
||||
console.error(error ?? `An unhandled rejection error occurred but error was null or undefined`);
|
||||
|
||||
if (!error) return;
|
||||
|
||||
// ReferenceError: publishMessage is not defined
|
||||
/*
|
||||
const embeds = new Embeds()
|
||||
.setDescription(["```js", error, "```"].join(`\n`))
|
||||
.setTimestamp()
|
||||
.setFooter("Unhandled Rejection Error Occurred");
|
||||
|
||||
// SEND ERROR TO THE LOG CHANNEL ON THE DEV SERVER
|
||||
return bot.helpers.sendWebhookMessage(bot.transformers.snowflake(id), token, { embeds }).catch(console.error);
|
||||
*/
|
||||
})
|
||||
.on('uncaughtException', async (error) => {
|
||||
if (!BUGS_ERRORS_REPORT_WEBHOOK) return;
|
||||
const { id, token } = webhookURLToIDAndToken(BUGS_ERRORS_REPORT_WEBHOOK);
|
||||
if (!id || !token) return;
|
||||
|
||||
// DO NOT SEND ERRORS FROM NON PRODUCTION
|
||||
if (BOT_ID !== 270010330782892032n) {
|
||||
return console.error(error);
|
||||
}
|
||||
|
||||
// An unhandled error occurred on the bot in production
|
||||
console.error(error ?? `An unhandled exception occurred but error was null or undefined`);
|
||||
|
||||
if (!error) process.exit(1);
|
||||
|
||||
/*
|
||||
const embeds = new Embeds()
|
||||
.setDescription(["```js", error.stack, "```"].join(`\n`))
|
||||
.setTimestamp()
|
||||
.setFooter("Unhandled Exception Error Occurred");
|
||||
// SEND ERROR TO THE LOG CHANNEL ON THE DEV SERVER
|
||||
await bot.helpers.sendWebhookMessage(bot.transformers.snowflake(id), token, { embeds }).catch(console.error);
|
||||
*/
|
||||
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
if (process.env.DEVELOPMENT === 'true') {
|
||||
bot.logger.info(`[DEV MODE] Updating slash commands for dev server.`);
|
||||
updateDevCommands(bot);
|
||||
if (MESSAGEQUEUE_ENABLE) {
|
||||
await connectToRabbitMQ()
|
||||
}
|
||||
|
||||
// Handle events from the gateway
|
||||
const handleEvent = async (message: DiscordGatewayPayload, shardId: number) => {
|
||||
// EMITS RAW EVENT
|
||||
bot.events.raw(bot, message, shardId);
|
||||
const app = buildFastifyApp()
|
||||
|
||||
if (message.t && message.t !== 'RESUMED') {
|
||||
// When a guild or something isnt in cache this will fetch it before doing anything else
|
||||
if (!['READY', 'GUILD_LOADED_DD'].includes(message.t)) {
|
||||
await bot.events.dispatchRequirements(bot, message, shardId);
|
||||
}
|
||||
app.get('/timecheck', async (_req, res) => {
|
||||
res.status(200).send({ message: Date.now() })
|
||||
})
|
||||
|
||||
bot.handlers[message.t]?.(bot, message, shardId);
|
||||
}
|
||||
};
|
||||
|
||||
const app = express();
|
||||
|
||||
app.use(
|
||||
express.urlencoded({
|
||||
extended: true,
|
||||
}),
|
||||
);
|
||||
|
||||
app.use(express.json());
|
||||
|
||||
app.all('/', async (req, res) => {
|
||||
try {
|
||||
if (!EVENT_HANDLER_AUTHORIZATION || EVENT_HANDLER_AUTHORIZATION !== req.headers.authorization) {
|
||||
return res.status(401).json({ error: 'Invalid authorization key.' });
|
||||
}
|
||||
|
||||
const json = req.body as {
|
||||
message: DiscordGatewayPayload;
|
||||
shardId: number;
|
||||
};
|
||||
|
||||
await handleEvent(json.message, json.shardId);
|
||||
|
||||
res.status(200).json({ success: true });
|
||||
} catch (error: any) {
|
||||
bot.logger.error(error);
|
||||
res.status(error.code).json(error);
|
||||
}
|
||||
});
|
||||
|
||||
app.listen(EVENT_HANDLER_PORT, () => {
|
||||
console.log(`Bot is listening at ${EVENT_HANDLER_URL};`);
|
||||
});
|
||||
|
||||
const connectRabbitmq = async () => {
|
||||
let connection: amqplib.Connection | undefined;
|
||||
app.post('/', async (req, res) => {
|
||||
const body = req.body as GatewayEvent
|
||||
|
||||
try {
|
||||
connection = await amqplib.connect(
|
||||
`amqp://${process.env.MESSAGEQUEUE_USERNAME}:${process.env.MESSAGEQUEUE_PASSWORD}@${process.env.MESSAGEQUEUE_URL}`,
|
||||
);
|
||||
handleGatewayEvent(body.payload, body.shardId)
|
||||
|
||||
res.status(200).send()
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
bot.logger.error('There was an error handling the incoming gateway command', error)
|
||||
res.status(500).send()
|
||||
}
|
||||
})
|
||||
|
||||
if (!connection) return;
|
||||
connection.on('error', (err) => {
|
||||
console.error(err);
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
});
|
||||
await app.listen({
|
||||
host: EVENT_HANDLER_HOST,
|
||||
port: EVENT_HANDLER_PORT,
|
||||
})
|
||||
|
||||
bot.logger.info(`Bot event handler is listening on port ${EVENT_HANDLER_PORT}`)
|
||||
|
||||
async function handleGatewayEvent(payload: DiscordGatewayPayload, shardId: number): Promise<void> {
|
||||
bot.events.raw?.(payload, shardId)
|
||||
|
||||
// If we don't have the event type we don't process it further
|
||||
if (!payload.t) return
|
||||
|
||||
// Run the dispatch check
|
||||
await bot.events.dispatchRequirements?.(payload, shardId)
|
||||
|
||||
bot.handlers[payload.t as GatewayDispatchEventNames]?.(bot, payload, shardId)
|
||||
}
|
||||
|
||||
async function connectToRabbitMQ(): Promise<void> {
|
||||
const connection = await connectAmqp(`amqp://${MESSAGEQUEUE_USERNAME}:${MESSAGEQUEUE_PASSWORD}@${MESSAGEQUEUE_URL}`).catch((error) => {
|
||||
bot.logger.error('Failed to connect to RabbitMQ, retrying in 1s.', error)
|
||||
setTimeout(connectToRabbitMQ, 1000)
|
||||
})
|
||||
|
||||
if (!connection) return
|
||||
|
||||
connection.on('close', () => {
|
||||
setTimeout(connectRabbitmq, 1000);
|
||||
});
|
||||
setTimeout(connectToRabbitMQ, 1000)
|
||||
})
|
||||
connection.on('error', (error) => {
|
||||
bot.logger.error('There was an error in the connection with RabbitMQ, reconnecting in 1s.', error)
|
||||
setTimeout(connectToRabbitMQ, 1000)
|
||||
})
|
||||
|
||||
try {
|
||||
const channel = await connection.createChannel();
|
||||
const channel = await connection.createChannel().catch((error) => {
|
||||
bot.logger.error('There was an error creating the RabbitMQ channel', error)
|
||||
})
|
||||
|
||||
await channel.assertExchange('gatewayMessage', 'x-message-deduplication', {
|
||||
if (!channel) return
|
||||
|
||||
const exchange = await channel
|
||||
.assertExchange('gatewayMessage', 'x-message-deduplication', {
|
||||
durable: true,
|
||||
arguments: {
|
||||
'x-cache-size': 1000,
|
||||
'x-cache-ttl': 500,
|
||||
'x-cache-size': 1000, // maximum number of entries
|
||||
'x-cache-ttl': 500, // 500ms
|
||||
},
|
||||
});
|
||||
})
|
||||
.catch((error) => {
|
||||
bot.logger.error('There was an error asserting the exchange', error)
|
||||
})
|
||||
|
||||
await channel.assertQueue('gatewayMessageQueue');
|
||||
await channel.bindQueue('gatewayMessageQueue', 'gatewayMessage', '');
|
||||
await channel.consume(
|
||||
'gatewayMessageQueue',
|
||||
async (msg) => {
|
||||
if (!msg) return;
|
||||
const json = JSON.parse(msg.content.toString()) as {
|
||||
message: DiscordGatewayPayload;
|
||||
shardId: number;
|
||||
};
|
||||
if (!exchange) return
|
||||
|
||||
await handleEvent(json.message, json.shardId);
|
||||
await channel.assertQueue('gatewayMessageQueue').catch(bot.logger.error)
|
||||
await channel.bindQueue('gatewayMessageQueue', 'gatewayMessage', '').catch(bot.logger.error)
|
||||
await channel
|
||||
.consume('gatewayMessageQueue', async (message) => {
|
||||
if (!message) return
|
||||
|
||||
await channel.ack(msg);
|
||||
},
|
||||
{
|
||||
noAck: false,
|
||||
},
|
||||
);
|
||||
try {
|
||||
const messageBody = JSON.parse(message.content.toString()) as GatewayEvent
|
||||
|
||||
await handleGatewayEvent(messageBody.payload, messageBody.shardId)
|
||||
|
||||
channel.ack(message)
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
bot.logger.error('There was an error handling events received from RabbitMQ', error)
|
||||
}
|
||||
};
|
||||
|
||||
if (process.env.MESSAGEQUEUE_ENABLE === 'true') {
|
||||
connectRabbitmq();
|
||||
})
|
||||
.catch(bot.logger.error)
|
||||
}
|
||||
|
||||
interface GatewayEvent {
|
||||
payload: DiscordGatewayPayload
|
||||
shardId: number
|
||||
}
|
||||
|
||||
@@ -1,23 +0,0 @@
|
||||
const english = {
|
||||
// Permissions
|
||||
NEED_VIP: '❌ Only VIP users or servers can use this feature.',
|
||||
|
||||
// Execute Command
|
||||
EXECUTE_COMMAND_NOT_FOUND: '❌ Something went wrong. I was not able to find this command.',
|
||||
EXECUTE_COMMAND_ERROR: '❌ Something went wrong. The command execution has thrown an error.',
|
||||
|
||||
// Language Command
|
||||
LANGUAGE_NAME: 'language',
|
||||
LANGUAGE_DESCRIPTION: '⚙️ Change the bots language.',
|
||||
LANGUAGE_KEY_NAME: 'name',
|
||||
LANGUAGE_KEY_DESCRIPTION: 'What language would you like to set?',
|
||||
LANGUAGE_UPDATED: (language: string) => `The language has been updated to ${language}`,
|
||||
|
||||
// Ping Command
|
||||
PING_NAME: 'ping',
|
||||
PING_DESCRIPTION: '🏓 Check whether the bot is online and responsive.',
|
||||
PING_RESPONSE: '🏓 Pong! I am online and responsive! :clock10:',
|
||||
PING_RESPONSE_WITH_TIME: (time: number) => `🏓 Pong! ${time / 1000} seconds! I am online and responsive! :clock10:`,
|
||||
} as const;
|
||||
|
||||
export default english;
|
||||
@@ -1,20 +0,0 @@
|
||||
import english from './english.js';
|
||||
// import french from './french'
|
||||
// import german from './german'
|
||||
// import portuguese from './portuguese'
|
||||
// import russian from './russian'
|
||||
// import spanish from './spanish'
|
||||
|
||||
const languages: Record<LanguageNames, Language> & Record<string, Language> = {
|
||||
english,
|
||||
// french,
|
||||
// german,
|
||||
// portuguese,
|
||||
// russian,
|
||||
// spanish,
|
||||
};
|
||||
|
||||
export default languages;
|
||||
|
||||
export type Language = Record<string, string | string[] | ((...args: any[]) => string)>;
|
||||
export type LanguageNames = 'english';
|
||||
@@ -1,84 +0,0 @@
|
||||
import Embeds from 'discordeno/embeds';
|
||||
import { bot } from '../bot.js';
|
||||
import { webhookURLToIDAndToken } from '../utils/webhook.js';
|
||||
import type english from './english.js';
|
||||
import languages from './languages.js';
|
||||
|
||||
const MISSING_TRANSLATION_WEBHOOK = process.env.MISSING_TRANSLATION_WEBHOOK;
|
||||
|
||||
/** This should hold the language names per guild id. <guildId, language> */
|
||||
export const serverLanguages = new Map<bigint, keyof typeof languages>();
|
||||
|
||||
export function translate<K extends translationKeys>(
|
||||
guildIdOrLanguage: bigint | keyof typeof languages,
|
||||
key: K,
|
||||
...params: getArgs<K>
|
||||
): string {
|
||||
const language = getLanguage(guildIdOrLanguage);
|
||||
let value: string | ((...any: any[]) => string) | string[] | undefined = languages[language]?.[key];
|
||||
|
||||
// Was not able to be translated
|
||||
if (!value) {
|
||||
// Check if this key is available in english
|
||||
if (language !== 'english') {
|
||||
value = languages.english[key];
|
||||
}
|
||||
|
||||
// Still not found in english so default to using the KEY_ITSELF
|
||||
if (!value) value = key;
|
||||
|
||||
// Send a log webhook so the devs know sth is missing
|
||||
missingTranslation(language, key);
|
||||
}
|
||||
|
||||
if (Array.isArray(value)) return value.join('\n');
|
||||
|
||||
if (typeof value === 'function') return value(...(params || []));
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
/** Get the language this guild has set, will always return "english" if it is not in cache */
|
||||
export function getLanguage(guildIdOrLanguage: bigint | keyof typeof languages) {
|
||||
return typeof guildIdOrLanguage === 'string'
|
||||
? guildIdOrLanguage
|
||||
: serverLanguages.get(guildIdOrLanguage) ?? 'english';
|
||||
}
|
||||
|
||||
export async function loadLanguage(guildId: bigint) {
|
||||
// TODO: add this settings
|
||||
// const settings = await database.findOne('guilds', guildId)
|
||||
const settings = { language: 'undefined' };
|
||||
|
||||
if (settings?.language && languages[settings.language]) {
|
||||
serverLanguages.set(guildId, settings.language);
|
||||
} else serverLanguages.set(guildId, 'english');
|
||||
}
|
||||
|
||||
/** Send a webhook for a missing translation key */
|
||||
export async function missingTranslation(language: keyof typeof languages, key: string) {
|
||||
if (!MISSING_TRANSLATION_WEBHOOK) return;
|
||||
const { id, token } = webhookURLToIDAndToken(MISSING_TRANSLATION_WEBHOOK);
|
||||
if (!id || !token) return;
|
||||
|
||||
const embeds = new Embeds()
|
||||
.setTitle('Missing Translation')
|
||||
.setColor('RANDOM')
|
||||
.addField('Language', language, true)
|
||||
.addField('Key', key, true);
|
||||
|
||||
await bot.helpers
|
||||
.sendWebhookMessage(bot.transformers.snowflake(id), token, {
|
||||
// SETUP-DD-TEMP: If you wish to make it @ mention you, please edit the next line.
|
||||
// content: `<@${owner id here}>`,
|
||||
embeds,
|
||||
wait: false,
|
||||
})
|
||||
.catch(bot.logger.error);
|
||||
}
|
||||
|
||||
// type translationKeys = keyof typeof english | string
|
||||
export type translationKeys = keyof typeof english;
|
||||
type getArgs<K extends translationKeys> = (typeof english)[K] extends (...any: any[]) => unknown
|
||||
? Parameters<(typeof english)[K]>
|
||||
: [];
|
||||
5
examples/bigbot/src/bot/register-commands.ts
Normal file
5
examples/bigbot/src/bot/register-commands.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import 'dotenv/config'
|
||||
|
||||
import { updateCommands } from './utils/updateCommands.js'
|
||||
|
||||
await updateCommands()
|
||||
@@ -1,12 +0,0 @@
|
||||
// This file allows you to tell typescript about any additions you have made to the internal discordeno objects.
|
||||
import type { Interaction, InteractionCallbackData, InteractionResponse, Message } from 'discordeno';
|
||||
|
||||
export interface InteractionWithCustomProps extends Interaction {
|
||||
// Normally, to send a response you would have to do something like bot.helpers.sendInteractionResponse(interaction.id, interaction.token, { type: InteractionResponseTypes.ChannelMessageWithSource, data: { content: "text here" } })
|
||||
// But with this reply method we added, it is as simple as interaction.reply("text here").
|
||||
// Feel free to delete these comments once you have understood the concept.
|
||||
/** Send a reply to an interaction. */
|
||||
reply: (response: InteractionResponse | string) => Promise<Message | undefined>;
|
||||
/** Edit a deferred reply of an interaction. */
|
||||
editReply: (response: InteractionCallbackData | string) => Promise<Message | undefined>;
|
||||
}
|
||||
@@ -1,151 +0,0 @@
|
||||
import type { Interaction, Member, Message } from 'discordeno';
|
||||
import { bot } from '../bot.js';
|
||||
|
||||
export async function needMessage(
|
||||
memberId: bigint,
|
||||
channelId: bigint,
|
||||
options: MessageCollectorOptions & { amount?: 1 },
|
||||
): Promise<Message>;
|
||||
export async function needMessage(
|
||||
memberId: bigint,
|
||||
channelId: bigint,
|
||||
options: MessageCollectorOptions & { amount?: number },
|
||||
): Promise<Message[]>;
|
||||
export async function needMessage(memberId: bigint, channelId: bigint): Promise<Message>;
|
||||
export async function needMessage(memberId: bigint, channelId: bigint, options?: MessageCollectorOptions) {
|
||||
const messages = await collectMessages({
|
||||
key: memberId,
|
||||
channelId,
|
||||
createdAt: Date.now(),
|
||||
filter: options?.filter || ((msg) => memberId === msg.authorId),
|
||||
amount: options?.amount || 1,
|
||||
duration: options?.duration || 1000 * 60 * 5,
|
||||
});
|
||||
|
||||
return (options?.amount || 1) > 1 ? messages : messages[0];
|
||||
}
|
||||
|
||||
export async function collectMessages(options: CollectMessagesOptions): Promise<Message[]> {
|
||||
return await new Promise((resolve, reject) => {
|
||||
bot.collectors.messages
|
||||
.get(options.key)
|
||||
?.reject('A new collector began before the user responded to the previous one.');
|
||||
|
||||
bot.collectors.messages.set(options.key, {
|
||||
...options,
|
||||
messages: [],
|
||||
resolve,
|
||||
reject,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
export function processMessageCollectors(message: Message) {
|
||||
// IGNORE DMS
|
||||
if (!message.guildId) return;
|
||||
|
||||
const collector = bot.collectors.messages.get(message.authorId);
|
||||
// This user has no collectors pending or the message is in a different channel
|
||||
if (!collector || message.channelId !== collector.channelId) return;
|
||||
// This message is a response to a collector. Now running the filter function.
|
||||
if (!collector.filter(message)) return;
|
||||
|
||||
// If the necessary amount has been collected
|
||||
if (collector.amount === 1 || collector.amount === collector.messages.length + 1) {
|
||||
// Remove the collector
|
||||
bot.collectors.messages.delete(message.authorId);
|
||||
// Resolve the collector
|
||||
return collector.resolve([...collector.messages, message]);
|
||||
}
|
||||
|
||||
// More messages still need to be collected
|
||||
collector.messages.push(message);
|
||||
}
|
||||
|
||||
export interface BaseCollectorOptions {
|
||||
/** The amount of messages to collect before resolving. Defaults to 1 */
|
||||
amount?: number;
|
||||
/** The amount of milliseconds this should collect for before expiring. Defaults to 5 minutes. */
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface MessageCollectorOptions extends BaseCollectorOptions {
|
||||
/** Function that will filter messages to determine whether to collect this message. Defaults to making sure the message is sent by the same member. */
|
||||
filter?: (message: Message) => boolean;
|
||||
/** The amount of messages to collect before resolving. Defaults to 1 */
|
||||
amount?: number;
|
||||
/** The amount of milliseconds this should collect for before expiring. Defaults to 5 minutes. */
|
||||
duration?: number;
|
||||
}
|
||||
|
||||
export interface ReactionCollectorOptions extends BaseCollectorOptions {
|
||||
/** Function that will filter messages to determine whether to collect this message. Defaults to making sure the message is sent by the same member. */
|
||||
filter?: (userId: bigint, reaction: string, message: Message | { id: string }) => boolean;
|
||||
}
|
||||
|
||||
export interface BaseCollectorCreateOptions {
|
||||
/** The unique key that will be used to get responses for this. Ideally, meant to be for member id. */
|
||||
key: bigint;
|
||||
/** The amount of messages to collect before resolving. */
|
||||
amount: number;
|
||||
/** The timestamp when this collector was created */
|
||||
createdAt: number;
|
||||
/** The duration in milliseconds how long this collector should last. */
|
||||
duration: number;
|
||||
}
|
||||
|
||||
export interface CollectMessagesOptions extends BaseCollectorCreateOptions {
|
||||
/** The channel Id where this is listening to */
|
||||
channelId: bigint;
|
||||
/** Function that will filter messages to determine whether to collect this message */
|
||||
filter: (message: Message) => boolean;
|
||||
}
|
||||
|
||||
export interface CollectReactionsOptions extends BaseCollectorCreateOptions {
|
||||
/** The message Id where this is listening to */
|
||||
messageId: bigint;
|
||||
/** Function that will filter messages to determine whether to collect this message */
|
||||
filter: (userId: bigint, reaction: string, message: Message | { id: string }) => boolean;
|
||||
}
|
||||
|
||||
export interface MessageCollector extends CollectMessagesOptions {
|
||||
resolve: (value: Message[] | PromiseLike<Message[]>) => void;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
reject: (reason?: any) => void;
|
||||
/** Where the messages are stored if the amount to collect is more than 1. */
|
||||
messages: Message[];
|
||||
}
|
||||
|
||||
export interface ReactionCollector extends CollectReactionsOptions {
|
||||
resolve: (value: string[] | PromiseLike<string[]>) => void;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
reject: (reason?: any) => void;
|
||||
/** Where the reactions are stored if the amount to collect is more than 1. */
|
||||
reactions: string[];
|
||||
}
|
||||
|
||||
export interface CollectButtonOptions extends BaseCollectorCreateOptions {
|
||||
/** The message Id where this is listening to */
|
||||
messageId: bigint;
|
||||
/** Function that will filter messages to determine whether to collect this message */
|
||||
filter: (message: Message, member?: Member) => boolean;
|
||||
}
|
||||
|
||||
export interface ButtonCollector extends CollectButtonOptions {
|
||||
resolve: (value: ButtonCollectorReturn[] | PromiseLike<ButtonCollectorReturn[]>) => void;
|
||||
// deno-lint-ignore no-explicit-any
|
||||
reject: (reason?: any) => void;
|
||||
/** Where the buttons are stored if the amount to collect is more than 1. */
|
||||
buttons: ButtonCollectorReturn[];
|
||||
}
|
||||
|
||||
export interface ButtonCollectorOptions extends BaseCollectorOptions {
|
||||
/** Function that will filter messages to determine whether to collect this message. Defaults to making sure the message is sent by the same member. */
|
||||
filter?: (message: Message, member?: Member) => boolean;
|
||||
}
|
||||
|
||||
export interface ButtonCollectorReturn {
|
||||
customId: string;
|
||||
interaction: Omit<Interaction, 'member'>;
|
||||
member?: Member;
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
import type { BotWithCustomProps } from '../../bot.js';
|
||||
import { customizeTransformers } from './transformers/mod.js';
|
||||
|
||||
export function customizeInternals(bot: BotWithCustomProps) {
|
||||
customizeTransformers(bot);
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
// SETUP-DD-TEMP: This file is an example, of how to customize an object properties that you do not want.
|
||||
// Only keep the properties your bot uses. If your bot does not use emojis in cache, you can save all that memory.
|
||||
// This file is currently disabled, but you can enable it should you choose when you go the customizer file.
|
||||
// Feel free to delete this comment or file as you wish.
|
||||
import type { Guild } from 'discordeno';
|
||||
import { Collection } from 'discordeno';
|
||||
import type { BotWithCustomProps } from '../../../bot.js';
|
||||
|
||||
export function customizeGuildTransformer(bot: BotWithCustomProps) {
|
||||
bot.transformers.guild = function (bot, payload) {
|
||||
const guildId = bot.transformers.snowflake(payload.guild.id);
|
||||
|
||||
return {
|
||||
name: payload.guild.name,
|
||||
joinedAt: payload.guild.joined_at ? Date.parse(payload.guild.joined_at) : undefined,
|
||||
memberCount: payload.guild.member_count ?? 0,
|
||||
shardId: payload.shardId,
|
||||
icon: payload.guild.icon ? bot.utils.iconHashToBigInt(payload.guild.icon) : undefined,
|
||||
roles: new Collection(
|
||||
payload.guild.roles?.map((role) => {
|
||||
const result = bot.transformers.role(bot, { role, guildId });
|
||||
return [result.id, result];
|
||||
}),
|
||||
),
|
||||
id: guildId,
|
||||
ownerId: bot.transformers.snowflake(payload.guild.owner_id),
|
||||
} as unknown as Guild;
|
||||
};
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
// SETUP-DD-TEMP: This file serves as an example, of how to customize internal discordeno objects. Feel free to use, add more or remove as desired.
|
||||
import type { InteractionCallbackData, InteractionResponse } from 'discordeno';
|
||||
import { InteractionResponseTypes } from 'discordeno';
|
||||
import type { BotWithCustomProps } from '../../../bot.js';
|
||||
|
||||
export function customizeInteractionTransformer(bot: BotWithCustomProps) {
|
||||
// Store the internal transformer function
|
||||
const oldInteraction = bot.transformers.interaction;
|
||||
|
||||
// Overwrite the internal function.
|
||||
bot.transformers.interaction = function (_, payload) {
|
||||
// Run the old function to get the internal value.
|
||||
const interaction = oldInteraction(bot, payload);
|
||||
|
||||
// Add anything to this object. In this case we add a Interaction.reply() method.
|
||||
Object.defineProperty(interaction, 'reply', {
|
||||
value: function (response: InteractionResponse | string) {
|
||||
if (typeof response === 'string') {
|
||||
response = { type: InteractionResponseTypes.ChannelMessageWithSource, data: { content: response } };
|
||||
}
|
||||
|
||||
return bot.helpers.sendInteractionResponse(interaction.id, interaction.token, response);
|
||||
},
|
||||
});
|
||||
Object.defineProperty(interaction, 'editReply', {
|
||||
value: function (response: InteractionCallbackData | string) {
|
||||
if (typeof response === 'string') {
|
||||
response = { content: response };
|
||||
}
|
||||
|
||||
return bot.helpers.editOriginalInteractionResponse(interaction.token, response);
|
||||
},
|
||||
});
|
||||
// Add as many properties or methods you would like here.
|
||||
// NOTE: Whenever you add anything here, in order to get nice autocomplete you should also add it to the src/types/discordeno.ts file.
|
||||
|
||||
// Return the new customized object.
|
||||
return interaction;
|
||||
};
|
||||
}
|
||||
@@ -1,10 +0,0 @@
|
||||
import type { BotWithCustomProps } from '../../../bot.js';
|
||||
// SETUP-DD-TEMP: Enable this comment if you want to enable this customizer.
|
||||
// import { customizeGuildTransformer } from "./guild.js";
|
||||
import { customizeInteractionTransformer } from './interaction.js';
|
||||
|
||||
export function customizeTransformers(bot: BotWithCustomProps) {
|
||||
customizeInteractionTransformer(bot);
|
||||
// SETUP-DD-TEMP: Enable this comment if you want to enable this customizer.
|
||||
// customizeGuildTransformer(bot);
|
||||
}
|
||||
13
examples/bigbot/src/bot/utils/loader.ts
Normal file
13
examples/bigbot/src/bot/utils/loader.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { readdir } from 'node:fs/promises'
|
||||
import { bot } from '../bot.js'
|
||||
|
||||
export default async function importDirectory(folder: string): Promise<void> {
|
||||
const files = await readdir(folder, { recursive: true })
|
||||
|
||||
for (const filename of files) {
|
||||
if (!filename.endsWith('.js')) continue
|
||||
|
||||
// Using `file://` to avoid weird issues with paths on Windows
|
||||
await import(`file://${folder}/${filename}`).catch((x) => bot.logger.fatal(`Cannot import file (${folder}/${filename}) for reason:`, x))
|
||||
}
|
||||
}
|
||||
@@ -1,307 +0,0 @@
|
||||
import type {
|
||||
ApplicationCommandOptionTypes,
|
||||
ApplicationCommandTypes,
|
||||
Bot,
|
||||
Channel,
|
||||
Interaction,
|
||||
Member,
|
||||
PermissionStrings,
|
||||
Role,
|
||||
User,
|
||||
} from 'discordeno';
|
||||
import type english from '../../languages/english.js';
|
||||
import type { translationKeys } from '../../languages/translate.js';
|
||||
import type { InteractionWithCustomProps } from '../../typings/discordeno.js';
|
||||
import type { PermissionLevelHandlers } from './permLevels.js';
|
||||
|
||||
export function createCommand<T extends readonly ArgumentDefinition[]>(command: Command<T>) {
|
||||
return command;
|
||||
}
|
||||
|
||||
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;
|
||||
|
||||
type Identity<T> = { [P in keyof T]: T[P] };
|
||||
|
||||
// TODO: make required by default true
|
||||
// Define each of the types here
|
||||
interface BaseDefinition {
|
||||
description: translationKeys;
|
||||
}
|
||||
|
||||
// Subcommand
|
||||
type SubcommandArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.SubCommand;
|
||||
// options: Omit<ArgumentDefinition, 'SubcommandArgumentDefinition' | 'SubcommandGroupArgumentDefinition'>[]
|
||||
options?: readonly ArgumentDefinition[];
|
||||
};
|
||||
|
||||
// SubcommandGroup
|
||||
type SubcommandGroupArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.SubCommandGroup;
|
||||
options: readonly SubcommandArgumentDefinition[];
|
||||
};
|
||||
|
||||
// String
|
||||
type StringArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.String;
|
||||
choices?: ReadonlyArray<{ name: string; value: string }>;
|
||||
required?: true;
|
||||
};
|
||||
type StringOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.String;
|
||||
choices?: ReadonlyArray<{ name: string; value: string }>;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// Integer
|
||||
type IntegerArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Integer;
|
||||
choices?: ReadonlyArray<{ name: string; value: number }>;
|
||||
required: true;
|
||||
};
|
||||
type IntegerOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Integer;
|
||||
choices?: ReadonlyArray<{ name: string; value: number }>;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// BOOLEAN
|
||||
type BooleanArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Boolean;
|
||||
required: true;
|
||||
};
|
||||
type BooleanOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Boolean;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// USER
|
||||
type UserArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.User;
|
||||
required: true;
|
||||
};
|
||||
type UserOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.User;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// CHANNEL
|
||||
type ChannelArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Channel;
|
||||
required: true;
|
||||
};
|
||||
type ChannelOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Channel;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// ROLE
|
||||
type RoleArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Role;
|
||||
required: true;
|
||||
};
|
||||
type RoleOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Role;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// MENTIONABLE
|
||||
type MentionableArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Mentionable;
|
||||
required: true;
|
||||
};
|
||||
type MentionableOptionalArgumentDefinition<N extends translationKeys = translationKeys> = BaseDefinition & {
|
||||
name: N;
|
||||
type: ApplicationCommandOptionTypes.Mentionable;
|
||||
required?: false;
|
||||
};
|
||||
|
||||
// Add each of known ArgumentDefinitions to this union.
|
||||
export type ArgumentDefinition =
|
||||
| StringArgumentDefinition
|
||||
| StringOptionalArgumentDefinition
|
||||
| IntegerArgumentDefinition
|
||||
| IntegerOptionalArgumentDefinition
|
||||
| BooleanArgumentDefinition
|
||||
| BooleanOptionalArgumentDefinition
|
||||
| UserArgumentDefinition
|
||||
| UserOptionalArgumentDefinition
|
||||
| ChannelArgumentDefinition
|
||||
| ChannelOptionalArgumentDefinition
|
||||
| RoleArgumentDefinition
|
||||
| RoleOptionalArgumentDefinition
|
||||
| MentionableArgumentDefinition
|
||||
| MentionableOptionalArgumentDefinition
|
||||
| SubcommandArgumentDefinition
|
||||
| SubcommandGroupArgumentDefinition;
|
||||
|
||||
type getName<K extends translationKeys> = (typeof english)[K] extends string ? (typeof english)[K] : never;
|
||||
|
||||
// OPTIONALS MUST BE FIRST!!!
|
||||
export type ConvertArgumentDefinitionsToArgs<T extends readonly ArgumentDefinition[]> = Identity<
|
||||
UnionToIntersection<
|
||||
{
|
||||
[P in keyof T]: T[P] extends StringOptionalArgumentDefinition<infer N> // STRING
|
||||
? {
|
||||
// @ts-expect-error TODO: fix this some day
|
||||
[_ in getName<N>]?: T[P]['choices'] extends ReadonlyArray<{ name: string; value: string }> // @ts-expect-error
|
||||
? T[P]['choices'][number]['value']
|
||||
: string;
|
||||
}
|
||||
: T[P] extends StringArgumentDefinition<infer N>
|
||||
? {
|
||||
// @ts-expect-error TODO: fix this some day
|
||||
[_ in getName<N>]: T[P]['choices'] extends ReadonlyArray<{ name: string; value: string }> // @ts-expect-error
|
||||
? T[P]['choices'][number]['value']
|
||||
: string;
|
||||
}
|
||||
: // INTEGER
|
||||
T[P] extends IntegerOptionalArgumentDefinition<infer N>
|
||||
? {
|
||||
[_ in getName<N>]?: T[P]['choices'] extends ReadonlyArray<{ name: string; value: number }> // @ts-expect-error
|
||||
? T[P]['choices'][number]['value']
|
||||
: number;
|
||||
}
|
||||
: T[P] extends IntegerArgumentDefinition<infer N>
|
||||
? {
|
||||
[_ in getName<N>]: T[P]['choices'] extends ReadonlyArray<{ name: string; value: number }> // @ts-expect-error
|
||||
? T[P]['choices'][number]['value']
|
||||
: number;
|
||||
}
|
||||
: // BOOLEAN
|
||||
T[P] extends BooleanOptionalArgumentDefinition<infer N>
|
||||
? { [_ in getName<N>]?: boolean }
|
||||
: T[P] extends BooleanArgumentDefinition<infer N>
|
||||
? { [_ in getName<N>]: boolean }
|
||||
: // USER
|
||||
T[P] extends UserOptionalArgumentDefinition<infer N>
|
||||
? {
|
||||
[_ in getName<N>]?: {
|
||||
user: User;
|
||||
member: Member;
|
||||
};
|
||||
}
|
||||
: T[P] extends UserArgumentDefinition<infer N>
|
||||
? {
|
||||
[_ in getName<N>]: {
|
||||
user: User;
|
||||
member: Member;
|
||||
};
|
||||
}
|
||||
: // CHANNEL
|
||||
T[P] extends ChannelOptionalArgumentDefinition<infer N>
|
||||
? { [_ in getName<N>]?: Channel }
|
||||
: T[P] extends ChannelArgumentDefinition<infer N>
|
||||
? { [_ in getName<N>]: Channel }
|
||||
: // ROLE
|
||||
T[P] extends RoleOptionalArgumentDefinition<infer N>
|
||||
? { [_ in getName<N>]?: Role }
|
||||
: T[P] extends RoleArgumentDefinition<infer N>
|
||||
? { [_ in getName<N>]: Role }
|
||||
: // MENTIONABLE
|
||||
T[P] extends MentionableOptionalArgumentDefinition<infer N>
|
||||
? {
|
||||
[_ in getName<N>]?:
|
||||
| Role
|
||||
| {
|
||||
user: User;
|
||||
member: Member;
|
||||
};
|
||||
}
|
||||
: T[P] extends MentionableArgumentDefinition<infer N>
|
||||
? {
|
||||
[_ in getName<N>]:
|
||||
| Role
|
||||
| {
|
||||
user: User;
|
||||
member: Member;
|
||||
};
|
||||
}
|
||||
: // SUBCOMMAND
|
||||
T[P] extends SubcommandArgumentDefinition<infer N>
|
||||
? {
|
||||
[_ in getName<N>]?: T[P]['options'] extends readonly ArgumentDefinition[] // @ts-expect-error somehow this check does not work
|
||||
? ConvertArgumentDefinitionsToArgs<T[P]['options']>
|
||||
: {};
|
||||
}
|
||||
: // SUBCOMMANDGROUP
|
||||
T[P] extends SubcommandGroupArgumentDefinition<infer N>
|
||||
? {
|
||||
[_ in getName<N>]?: ConvertArgumentDefinitionsToArgs<T[P]['options']>;
|
||||
}
|
||||
: never;
|
||||
}[number]
|
||||
>
|
||||
>;
|
||||
|
||||
export interface Command<T extends readonly ArgumentDefinition[]> {
|
||||
/** The name of the command, used for both slash and message commands. */
|
||||
name: translationKeys;
|
||||
/** The type of command. */
|
||||
type?: ApplicationCommandTypes;
|
||||
/** The description of the command */
|
||||
description: translationKeys;
|
||||
// TODO: consider type being a string like "number" | "user" for better ux
|
||||
/** The options for the command, used for both slash and message commands. */
|
||||
// options?: ApplicationCommandOption[];
|
||||
options?: T;
|
||||
execute: (bot: Bot, data: InteractionWithCustomProps, args: ConvertArgumentDefinitionsToArgs<T>) => unknown;
|
||||
subcommands?: Record<string, Omit<Command<any>, 'subcommands'> & { group?: string }>;
|
||||
/** Whether the command should have a cooldown */
|
||||
cooldown?: {
|
||||
/** How long the user needs to wait after the first execution until he can use the command again */
|
||||
seconds: number;
|
||||
/** How often the user is allowed to use the command until he is in cooldown */
|
||||
allowedUses?: number;
|
||||
};
|
||||
nsfw?: boolean;
|
||||
/** By default false */
|
||||
global?: boolean;
|
||||
/** Dm only by default false */
|
||||
dmOnly?: boolean;
|
||||
|
||||
/** VIP only by default false */
|
||||
vipOnly?: boolean;
|
||||
|
||||
advanced?: boolean;
|
||||
|
||||
/** Whether or not this slash command should be enabled right now. Defaults to true. */
|
||||
enabled?: boolean;
|
||||
/** Whether or not this command is still in development and should be setup in the dev server for testing. */
|
||||
dev?: boolean;
|
||||
/** Whether or not this command will take longer than 3s and need to acknowledge to discord. */
|
||||
acknowledge?: boolean;
|
||||
|
||||
permissionLevels?:
|
||||
| Array<keyof typeof PermissionLevelHandlers>
|
||||
| ((data: Interaction, command: Command<T>) => boolean | Promise<boolean>);
|
||||
botServerPermissions?: PermissionStrings[];
|
||||
botChannelPermissions?: PermissionStrings[];
|
||||
userServerPermissions?: PermissionStrings[];
|
||||
userChannelPermissions?: PermissionStrings[];
|
||||
}
|
||||
|
||||
export enum PermissionLevels {
|
||||
Member,
|
||||
Moderator,
|
||||
Admin,
|
||||
ServerOwner,
|
||||
BotSupporter,
|
||||
BotDev,
|
||||
BotOwner,
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import COMMANDS from '../../commands/mod.js';
|
||||
|
||||
export async function validateSlashLimits() {
|
||||
const MAX_ALLOWED_CHARACTERS = 4000;
|
||||
|
||||
const commands = await fetch('https://cmd-counter-play.deno.dev/', {
|
||||
body: JSON.stringify(COMMANDS),
|
||||
headers: {
|
||||
'content-type': 'application/json',
|
||||
},
|
||||
})
|
||||
.then(async (res) => await res.json())
|
||||
.catch(() => undefined);
|
||||
|
||||
if (!commands) return;
|
||||
|
||||
const invalidCommandNames: string[] = [];
|
||||
|
||||
if (commands[0]?.characters > MAX_ALLOWED_CHARACTERS) {
|
||||
for (const command of commands) {
|
||||
if (command.characters <= MAX_ALLOWED_CHARACTERS) continue;
|
||||
|
||||
invalidCommandNames.push(command.name);
|
||||
console.log(
|
||||
`[Invalid Command] The ${command.name} is not a valid command. It's total characters are (${command.characters}) which is more than the max allowed ${MAX_ALLOWED_CHARACTERS}.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (invalidCommandNames.length) throw new Error(`[Startup] Invalid commands: ${invalidCommandNames.join(', ')}`);
|
||||
}
|
||||
@@ -1,48 +0,0 @@
|
||||
import type { Interaction } from 'discordeno';
|
||||
import { validatePermissions } from 'discordeno/permissions-plugin';
|
||||
import type { Command } from './createCommand.js';
|
||||
|
||||
export default async function hasPermissionLevel(command: Command<any>, payload: Interaction) {
|
||||
// This command doesnt require a perm level so allow the command.
|
||||
if (!command.permissionLevels) return true;
|
||||
|
||||
// If a custom function was provided
|
||||
if (typeof command.permissionLevels === 'function') {
|
||||
return await command.permissionLevels(payload, command);
|
||||
}
|
||||
|
||||
// If an array of perm levels was provided
|
||||
for (const permlevel of command.permissionLevels) {
|
||||
// If this user has one of the allowed perm level, the loop is canceled and command is allowed.
|
||||
if (await PermissionLevelHandlers[permlevel](payload, command)) return true;
|
||||
}
|
||||
|
||||
// None of the perm levels were met. So cancel the command
|
||||
return false;
|
||||
}
|
||||
|
||||
export const PermissionLevelHandlers: Record<
|
||||
keyof typeof PermissionLevels,
|
||||
(payload: Interaction, command: Command<any>) => boolean | Promise<boolean>
|
||||
> = {
|
||||
MEMBER: () => true,
|
||||
MODERATOR: (payload) =>
|
||||
Boolean(payload.member?.permissions) && validatePermissions(payload.member!.permissions!, ['MANAGE_GUILD']),
|
||||
ADMIN: (payload) =>
|
||||
Boolean(payload.member?.permissions) && validatePermissions(payload.member!.permissions!, ['ADMINISTRATOR']),
|
||||
// TODO(cache): fix this
|
||||
SERVER_OWNER: () => false,
|
||||
BOT_SUPPORT: () => false,
|
||||
BOT_DEVS: () => false,
|
||||
BOT_OWNERS: (payload) => [130136895395987456n, 615542460151496705n].includes(payload.user.id),
|
||||
};
|
||||
|
||||
export enum PermissionLevels {
|
||||
MEMBER,
|
||||
MODERATOR,
|
||||
ADMIN,
|
||||
SERVER_OWNER,
|
||||
BOT_SUPPORT,
|
||||
BOT_DEVS,
|
||||
BOT_OWNERS,
|
||||
}
|
||||
@@ -1,149 +0,0 @@
|
||||
import type { ApplicationCommandOption, Bot } from 'discordeno';
|
||||
import { ApplicationCommandTypes } from 'discordeno';
|
||||
import { prisma } from '../../../prisma.js';
|
||||
import { bot } from '../../bot.js';
|
||||
import COMMANDS from '../../commands/mod.js';
|
||||
import { serverLanguages, translate } from '../../languages/translate.js';
|
||||
import type { ArgumentDefinition } from './createCommand.js';
|
||||
|
||||
const DEV_SERVER_ID = process.env.DEV_SERVER_ID as string;
|
||||
|
||||
export async function updateDevCommands(bot: Bot) {
|
||||
const cmds = Object.entries(COMMANDS)
|
||||
// ONLY DEV COMMANDS
|
||||
.filter(([_name, command]) => command?.dev);
|
||||
|
||||
if (!cmds.length) return;
|
||||
|
||||
// DEV RELATED COMMANDS, USE upsertGlobalApplicationCommands TO UPDATE GLOBALLY
|
||||
await bot.helpers.upsertGuildApplicationCommands(
|
||||
bot.transformers.snowflake(DEV_SERVER_ID),
|
||||
cmds.map(([name, command]) => {
|
||||
const translatedName = translate(DEV_SERVER_ID, command.name);
|
||||
const translatedDescription = command.description ? translate(DEV_SERVER_ID, command.description) : '';
|
||||
|
||||
if (command.type && command.type !== ApplicationCommandTypes.ChatInput) {
|
||||
return {
|
||||
name: (translatedName || name).toLowerCase(),
|
||||
type: command.type,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: (translatedName || name).toLowerCase(),
|
||||
description: translatedDescription || command.description,
|
||||
options: command.options
|
||||
? createOptions(bot.transformers.snowflake(DEV_SERVER_ID), command.options, command.name)
|
||||
: undefined,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// SETUP-DD-TEMP: You can make this able to be updated dynicamally by moving this value to something in the database and having a command to update it on the fly or as part of CI.
|
||||
export const CURRENT_SLASH_COMMAND_VERSION = 1;
|
||||
|
||||
/** Whether the guild has the latest slash command version */
|
||||
export async function usesLatestCommandVersion(guildId: bigint): Promise<boolean> {
|
||||
return (await getCurrentCommandVersion(guildId)) === CURRENT_SLASH_COMMAND_VERSION;
|
||||
}
|
||||
|
||||
/** Get the current slash command version for this guild */
|
||||
export async function getCurrentCommandVersion(guildId: bigint): Promise<number> {
|
||||
if (bot.commandVersions.has(guildId)) return bot.commandVersions.get(guildId)!;
|
||||
|
||||
const commandVersion = await prisma.commands.findUnique({ where: { id: guildId } });
|
||||
if (commandVersion) bot.commandVersions.set(guildId, commandVersion.version);
|
||||
|
||||
return commandVersion?.version ?? 0;
|
||||
}
|
||||
|
||||
export async function updateCommandVersion(guildId: bigint): Promise<number> {
|
||||
// UPDATE THE VERSION SAVED IN THE DB
|
||||
await prisma.commands.upsert({
|
||||
where: { id: guildId },
|
||||
create: { id: guildId, version: CURRENT_SLASH_COMMAND_VERSION },
|
||||
update: { version: CURRENT_SLASH_COMMAND_VERSION },
|
||||
});
|
||||
|
||||
bot.commandVersions.set(guildId, CURRENT_SLASH_COMMAND_VERSION);
|
||||
return CURRENT_SLASH_COMMAND_VERSION;
|
||||
}
|
||||
|
||||
export async function updateGuildCommands(bot: Bot, guildId: bigint) {
|
||||
if (guildId === 547046977578336286n) return await updateDevCommands(bot);
|
||||
|
||||
await updateCommandVersion(guildId);
|
||||
|
||||
// GUILD RELATED COMMANDS
|
||||
await bot.helpers.upsertGuildApplicationCommands(
|
||||
guildId,
|
||||
Object.entries(COMMANDS)
|
||||
// ONLY GUILD COMMANDS
|
||||
.filter(([_name, command]) => !command.global && !command.dev)
|
||||
.map(([name, command]) => {
|
||||
// USER OPTED TO USE BASIC VERSION ONLY
|
||||
if (command.advanced === false) {
|
||||
return {
|
||||
name,
|
||||
description: translate('english', command.description),
|
||||
options: command.options ? createOptions('english', command.options, command.name) : undefined,
|
||||
};
|
||||
}
|
||||
|
||||
// ADVANCED VERSION WILL ALLOW TRANSLATION
|
||||
const translatedName = translate(guildId, command.name);
|
||||
const translatedDescription = translate(guildId, command.description);
|
||||
|
||||
return {
|
||||
name: translatedName.toLowerCase(),
|
||||
description: translatedDescription,
|
||||
options: command.options ? createOptions(guildId, command.options, command.name) : undefined,
|
||||
};
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
// USED TO CACHE CONVERTED COMMANDS AFTER START TO PREVENT UNNECESSARY LOOPS
|
||||
const convertedCache = new Map<string, ApplicationCommandOption[]>();
|
||||
|
||||
/** Creates the commands options including subcommands. Also translates them. */
|
||||
function createOptions(
|
||||
guildId: bigint | 'english',
|
||||
options: readonly ArgumentDefinition[],
|
||||
commandName?: string,
|
||||
): ApplicationCommandOption[] | undefined {
|
||||
const language = guildId === 'english' ? 'english' : serverLanguages.get(guildId) ?? 'english';
|
||||
if (commandName && convertedCache.has(`${language}-${commandName}`)) {
|
||||
return convertedCache.get(`${language}-${commandName}`)!;
|
||||
}
|
||||
|
||||
const newOptions: ApplicationCommandOption[] = [];
|
||||
|
||||
for (const option of options || []) {
|
||||
const optionName = translate(guildId, option.name);
|
||||
const optionDescription = translate(guildId, option.description);
|
||||
|
||||
// TODO: remove this ts ignore
|
||||
// @ts-expect-error
|
||||
const choices = option.choices?.map((choice) => ({
|
||||
...choice,
|
||||
name: translate(guildId, choice.name),
|
||||
}));
|
||||
|
||||
newOptions.push({
|
||||
...option,
|
||||
name: optionName.toLowerCase(),
|
||||
description: optionDescription || 'No description available.',
|
||||
choices,
|
||||
// @ts-expect-error fix this
|
||||
options: option.options
|
||||
? // @ts-expect-error fix this
|
||||
createOptions(bot, guildId, option.options)
|
||||
: undefined,
|
||||
} as ApplicationCommandOption);
|
||||
}
|
||||
if (commandName) convertedCache.set(`${language}-${commandName}`, newOptions);
|
||||
|
||||
return newOptions;
|
||||
}
|
||||
19
examples/bigbot/src/bot/utils/updateCommands.ts
Normal file
19
examples/bigbot/src/bot/utils/updateCommands.ts
Normal file
@@ -0,0 +1,19 @@
|
||||
import assert from 'node:assert'
|
||||
import { DEVELOPMENT, DEV_SERVER_ID } from '../../config.js'
|
||||
import { bot } from '../bot.js'
|
||||
|
||||
export async function updateCommands(): Promise<void> {
|
||||
bot.logger.info('Updating commands')
|
||||
|
||||
const userCommands = bot.commands.filter((x) => !x.devOnly).array()
|
||||
await bot.helpers.upsertGlobalApplicationCommands(userCommands)
|
||||
|
||||
if (DEVELOPMENT) {
|
||||
assert(DEV_SERVER_ID, 'The DEV_SERVER_ID environment is missing')
|
||||
|
||||
bot.logger.info('Updating developer commands')
|
||||
|
||||
const devCommands = bot.commands.filter((x) => x.devOnly ?? false).array()
|
||||
await bot.helpers.upsertGuildApplicationCommands(DEV_SERVER_ID, devCommands)
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user