Migrate beginner and minimal bot to discordeno v19

This commit is contained in:
Fleny
2024-06-05 18:12:56 +02:00
parent 974ec745b2
commit 2ca34703b6
52 changed files with 4836 additions and 713 deletions

View File

@@ -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>

View File

@@ -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
View File

@@ -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

View File

@@ -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
}

View File

@@ -1,10 +1,13 @@
# Discordeno Bot Template
# Discordeno Bot Templates
In this directory you will find some example bots written using Discordeno.
## Minimal & Beginner
A very minimal bot with only a /ping command to show how to set-up a discordeno bot for interactions
## BigBot
![Log Image](https://i.imgur.com/09skKfz.png)
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).
[Website/Guide](https://discordeno.js.org/)
[Discord Server](https://discord.com/invite/5vBgXk3UcZ)
The BigBot template is intended for more complex systems that need scaling.

55
examples/beginner/.gitignore vendored Normal file
View File

@@ -0,0 +1,55 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Test file for testing dd changes
debug.ts
testing
# dependencies
node_modules
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
website/api_reference/generated/classes/*.md
website/api_reference/generated/enums/*.md
website/api_reference/generated/interfaces/*.md
website/api_reference/generated/modules/*.md
website/.yarn/
# testing
coverage
# build
dist
denoTestsDist
bunTestsDist
benchDist
out
build
.docusaurus
.cache-loader
/db
# 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
# turbo
.turbo

View File

@@ -2,17 +2,15 @@
This template is designed for the beginner developer to start coding discord bots.
Make sure to install the latest version when you use it.
## 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.
- Download the source
- Install yarn
- Rename the .env.example file to .env OR create a new .env file and copy the example file code to this new file.
- 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 `yarn start` to run the bot

View File

@@ -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()

View File

@@ -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!),
}

View File

@@ -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'

View File

@@ -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)

View File

@@ -0,0 +1,26 @@
{
"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.ad7e74c",
"chalk": "^5.3.0",
"dotenv": "^16.4.5"
},
"devDependencies": {
"@swc/cli": "^0.3.12",
"@swc/core": "^1.5.25",
"@types/node": "^20.14.2",
"typescript": "^5.4.5"
}
}

View File

@@ -0,0 +1,23 @@
import { Collection, Intents, createBot, type Bot } from '@discordeno/bot'
import { configs } from './config.js'
import type { Command } from './types/commands.js'
const rawBot = createBot({
token: configs.token,
intents: Intents.Guilds,
})
// Setup desired proprieties
rawBot.transformers.desiredProperties.interaction.id = true
rawBot.transformers.desiredProperties.interaction.type = true
rawBot.transformers.desiredProperties.interaction.data = true
rawBot.transformers.desiredProperties.interaction.token = true
export const bot = rawBot as BotWithCommands
// Create the command collection
bot.commands = new Collection()
export interface BotWithCommands extends Bot {
commands: Collection<string, Command>
}

View File

@@ -0,0 +1,6 @@
import { bot } from './bot.js'
import type { Command } from './types/commands.js'
export function createCommand(command: Command): void {
bot.commands.set(command.name, command)
}

View File

@@ -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)
}

View File

@@ -1,18 +1,13 @@
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'
createCommand({
name: 'ping',
description: 'Ping the Bot!',
type: ApplicationCommandTypes.ChatInput,
execute: async (Bot, interaction) => {
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}ms`)
},
})

View File

@@ -0,0 +1,17 @@
const token = process.env.BOT_TOKEN
const devGuildId = process.env.DEV_GUILD_ID
if (!token) throw new Error('Missing BOT_TOKEN environment variable')
if (!devGuildId) throw new Error('Missing DEV_GUILD_ID environment variable')
export const configs: Config = {
/** Get token from ENV variable */
token,
/** The server id where you develop your bot and want dev commands created. */
devGuildId: BigInt(devGuildId),
}
export interface Config {
token: string
devGuildId: bigint
}

View File

@@ -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!')

View File

@@ -1,14 +1,15 @@
import { Bot } from '../../bot.ts.js'
import { InteractionTypes } from '../../deps.ts.js'
import log from '../utils/logger.ts.js'
import { InteractionTypes } from '@discordeno/bot'
import { bot } from '../bot.js'
import logger from '../utils/logger.js'
Bot.events.interactionCreate = (_, interaction) => {
bot.events.interactionCreate = (interaction) => {
if (!interaction.data) return
switch (interaction.type) {
case InteractionTypes.ApplicationCommand:
log.info(`[Application Command] ${interaction.data.name} command executed.`)
Bot.commands.get(interaction.data.name!)?.execute(Bot, interaction)
logger.info(`[Application Command] ${interaction.data.name} command executed.`)
bot.commands.get(interaction.data.name)?.execute(interaction)
break
}
}

View File

@@ -1,16 +1,17 @@
import { Bot } from '../../bot.ts.js'
import log from '../utils/logger.ts.js'
import { bot } from '../bot.js'
import logger from '../utils/logger.js'
Bot.events.ready = (_, payload) => {
log.info(`[READY] Shard ID ${payload.shardId} of ${Bot.gateway.lastShardId + 1} shards is ready!`)
bot.events.ready = ({ shardId }) => {
logger.info(`[READY] Shard ${shardId} is ready!`)
if (payload.shardId === Bot.gateway.lastShardId) {
if (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.')
function botFullyReady(): void {
// Do stuff that you want that get execute only when the bot is fully online.
logger.info('[READY] Bot is fully online.')
}

View File

@@ -0,0 +1,19 @@
import 'dotenv/config'
import { bot } from './bot.js'
import importDirectory from './utils/loader.js'
import logger from './utils/logger.js'
import { updateApplicationCommands } from './utils/updateCommands.js'
logger.info('Starting bot...')
logger.info('Loading commands...')
await importDirectory('./dist/commands')
logger.info('Loading events...')
await importDirectory('./dist/events')
logger.info('Updating commands...')
await updateApplicationCommands()
await bot.start()

View File

@@ -1,5 +1,4 @@
import type { BotClient } from '../../bot.ts.js'
import type { ApplicationCommandOption, ApplicationCommandTypes, Interaction } from '../../deps.ts.js'
import type { ApplicationCommandOption, ApplicationCommandTypes, Interaction } from '@discordeno/bot'
export interface Command {
/** The name of this command. */
@@ -13,5 +12,5 @@ export interface Command {
/** The options for this command */
options?: ApplicationCommandOption[]
/** This will be executed when the command is run. */
execute: (bot: BotClient, interaction: Interaction) => unknown
execute: (interaction: Interaction) => unknown
}

View File

@@ -1,3 +0,0 @@
// This file will export all of the types in this directory.
export * from './commands.ts.js'

View File

@@ -1,3 +0,0 @@
export function snowflakeToTimestamp(id: bigint) {
return Number(id / 4194304n + 1420070400000n)
}

View File

@@ -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}";`,
)
continue
}
await importDirectory(currentPath)
// 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),
)
}
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 = []
}

View File

@@ -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
}

View File

@@ -1,17 +1,17 @@
import { Bot } from '../../bot.ts.js'
import { configs } from '../../configs.ts.js'
import { bot } from '../bot.js'
import { configs } from '../config.js'
export async function updateApplicationCommands() {
await Bot.helpers.upsertGlobalApplicationCommands(
Bot.commands
export async function updateApplicationCommands(): Promise<void> {
await bot.helpers.upsertGlobalApplicationCommands(
bot.commands
// ONLY GLOBAL COMMANDS
.filter((command) => !command.devOnly)
.array(),
)
await Bot.helpers.upsertGuildApplicationCommands(
await bot.helpers.upsertGuildApplicationCommands(
configs.devGuildId,
Bot.commands
bot.commands
// ONLY GLOBAL COMMANDS
.filter((command) => !!command.devOnly)
.array(),

View 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
}
}

2114
examples/beginner/yarn.lock Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -1,18 +0,0 @@
{
"lint": {
"rules": {
"tags": ["recommended"],
"include": ["ban-untagged-todo"],
"exclude": ["no-unused-vars"]
}
},
"fmt": {
"options": {
"useTabs": false,
"lineWidth": 120,
"indentWidth": 2,
"singleQuote": false,
"proseWrap": "preserve"
}
}
}

View File

@@ -1 +1 @@
BOT_TOKEN=''
BOT_TOKEN=''

55
examples/minimal/.gitignore vendored Normal file
View File

@@ -0,0 +1,55 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
# Test file for testing dd changes
debug.ts
testing
# dependencies
node_modules
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/sdks
!.yarn/versions
website/api_reference/generated/classes/*.md
website/api_reference/generated/enums/*.md
website/api_reference/generated/interfaces/*.md
website/api_reference/generated/modules/*.md
website/.yarn/
# testing
coverage
# build
dist
denoTestsDist
bunTestsDist
benchDist
out
build
.docusaurus
.cache-loader
/db
# 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
# turbo
.turbo

View File

@@ -2,12 +2,15 @@
Just the minimum to get a working bot using interactions.
Make sure to install the latest version when you use it.
## Setup
Just rename `.env.example` to `.env` and fill it with your bot token.
- Download the source
- Install yarn
- Rename the .env.example file to .env OR create a new .env file and copy the example file code to this new file.
- 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 `yarn start` to run the bot

View File

@@ -1,5 +0,0 @@
import { dotEnvConfig } from './deps.ts.js'
dotEnvConfig({ export: true })
export const BOT_TOKEN = process.env.BOT_TOKEN || ''
export const BOT_ID = BigInt(atob(BOT_TOKEN.split('.')[0]))

View File

@@ -1,4 +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'
export { config as dotEnvConfig } from 'https://deno.land/x/dotenv@v3.1.0/mod.ts'
export * from 'https://deno.land/std@0.117.0/fmt/colors.ts'

View File

@@ -1,46 +0,0 @@
import { ActivityTypes, createBot, enableCachePlugin, enableCacheSweepers, fastFileLoader, GatewayIntents, startBot } from './deps.ts.js'
import { BOT_ID, BOT_TOKEN } from './configs.ts.js'
import { logger } from './src/utils/logger.ts.js'
import { events } from './src/events/mod.ts.js'
import { updateCommands } from './src/utils/helpers.ts.js'
const log = logger({ name: 'Main' })
log.info('Starting Bot, this might take a while...')
const paths = ['./src/events', './src/commands']
await fastFileLoader(paths).catch((err) => {
log.fatal(`Unable to Import ${paths}`)
log.fatal(err)
Deno.exit(1)
})
export const bot = enableCachePlugin(
createBot({
token: BOT_TOKEN,
botId: BOT_ID,
intents: GatewayIntents.Guilds,
events,
}),
)
// @ts-nocheck: no-updated-depencdencies
enableCacheSweepers(bot)
bot.gateway.manager.createShardOptions.makePresence = (shardId: number) => {
return {
shardId,
status: 'online',
activities: [
{
name: 'Discordeno is the Best Lib',
type: ActivityTypes.Game,
createdAt: Date.now(),
},
],
}
}
await startBot(bot)
await updateCommands(bot)

View File

@@ -0,0 +1,27 @@
{
"name": "dd-minimal-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.ad7e74c",
"chalk": "^5.3.0",
"dd-cache-proxy": "^2.1.1",
"dotenv": "^16.4.5"
},
"devDependencies": {
"@swc/cli": "^0.3.12",
"@swc/core": "^1.5.25",
"@types/node": "^20.14.2",
"typescript": "^5.4.5"
}
}

View 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

View 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[]
}

View File

@@ -1,25 +0,0 @@
import type { ApplicationCommandOption, ApplicationCommandTypes, Bot, Interaction } from '../../deps.ts.js'
import { Collection } from '../../deps.ts.js'
export type subCommand = Omit<Command, 'subcommands'>
export interface subCommandGroup {
name: string
subCommands: subCommand[]
}
export interface Command {
name: string
description: string
usage?: string[]
options?: ApplicationCommandOption[]
type: ApplicationCommandTypes
/** Defaults to `Guild` */
scope?: 'Global' | 'Guild'
execute: (bot: Bot, interaction: Interaction) => unknown
subcommands?: Array<subCommandGroup | subCommand>
}
export const commands = new Collection<string, Command>()
export function createCommand(command: Command) {
commands.set(command.name, command)
}

View File

@@ -1,19 +1,15 @@
import { ApplicationCommandTypes, InteractionResponseTypes } from '../../deps.ts.js'
import { humanizeMilliseconds, 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,
scope: 'Global',
execute: async (bot, interaction) => {
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 ${ping}ms (${humanizeMilliseconds(ping)})`,
},
})
await interaction.respond(`🏓 Pong! Ping ${ping}ms (${humanizeMilliseconds(ping)})`)
},
})

View 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
}

View File

@@ -1,4 +1,4 @@
import { events } from './mod.ts.js'
import { updateGuildCommands } from '../utils/helpers.ts.js'
import { bot } from '../bot.js'
import { updateGuildCommands } from '../utils/helpers.js'
events.guildCreate = async (bot, guild) => await updateGuildCommands(bot, guild)
bot.events.guildCreate = async (guild) => await updateGuildCommands(bot, guild)

View File

@@ -1,225 +1,94 @@
import type { BotWithCache, Guild } from '../../deps.ts.js'
import { ApplicationCommandOptionTypes, bgBlack, bgYellow, black, green, red, white, yellow } from '../../deps.ts.js'
import { events } from './mod.ts.js'
import { logger } from '../utils/logger.ts.js'
import { getGuildFromId, isSubCommand, isSubCommandGroup } from '../utils/helpers.ts.js'
import type { Command } from '../commands/mod.ts.js'
import { commands } from '../commands/mod.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'
const log = logger({ name: 'Event: InteractionCreate' })
const logger = createLogger({ name: 'Event: InteractionCreate' })
events.interactionCreate = async (rawBot, interaction) => {
const bot = rawBot as BotWithCache
bot.events.interactionCreate = async (interaction) => {
if (!interaction.data || !interaction.id) return
if (interaction.data && interaction.id) {
let guildName = 'Direct Message'
let guild = {} as Guild
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(bot, interaction.guildId).catch((err) => {
log.error(err)
})
if (guildOrVoid) {
guild = guildOrVoid
guildName = guild.name
}
}
// 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)
})
log.info(
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${bgBlack(white(`Trigger`))}] by ${interaction.user.username}#${
interaction.user.discriminator
} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
)
let command: undefined | Command = interaction.data.name ? commands.get(interaction.data.name) : undefined
let commandName = command?.name
if (command !== undefined) {
if (interaction.data.name) {
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)
commandName += ` ${subCommandGroup.name} ${command?.name}`
// Normal
}
if (optionType === ApplicationCommandOptionTypes.SubCommandGroup) {
// 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
commandName += ` ${command?.name}`
}
}
try {
if (command) {
command.execute(rawBot, interaction)
log.info(
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${bgBlack(green(`Success`))}] by ${interaction.user.username}#${
interaction.user.discriminator
} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
)
} else {
throw ''
}
} catch (err) {
log.error(
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${bgBlack(red(`Error`))}] by ${interaction.user.username}#${
interaction.user.discriminator
} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
)
err.length ? log.error(err) : undefined
}
} else {
log.warn(
`[Command: ${bgYellow(black(String(interaction.data.name)))} - ${bgBlack(yellow(`Not Found`))}] by ${interaction.user.username}#${
interaction.user.discriminator
} in ${guildName}${guildName !== 'Direct Message' ? ` (${guild.id})` : ``}`,
)
}
if (guildOrVoid) {
guild = guildOrVoid
guildName = guild.name
}
}
}
/*
// Handle subcommands
let cmdName = cmd.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})` : ``}`,
)
// Group
if (interaction.data?.options?.[0].type === DiscordApplicationCommandOptionTypes.SubCommandGroup) {
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 (!cmd.subcommands) return;
if (!command.subcommands) return
// Try to find the subcommand group
const subCmdGroup = cmd.subcommands?.find((cmd) => cmd.name == interaction?.data?.options?.[0].name);
if (!subCmdGroup) return;
const subCommandGroup = command.subcommands?.find((command) => command.name === interaction.data?.options?.[0].name)
if (!subCommandGroup) return
if (isSubCommand(subCmdGroup)) 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;
const targetCmdName = interaction.data.options?.[0].options?.[0].name ?? interaction.data.options?.[0].options?.[0].name
if (!targetCmdName) return
// Try to find the command
cmd = subCmdGroup.subCommands.find((c) => c.name === targetCmdName);
command = subCommandGroup.subCommands.find((c) => c.name === targetCmdName)
}
cmdName += ` ${subCmdGroup.name} ${cmd?.name}`;
// Normal
} else if (interaction.data?.options?.[0].type === DiscordApplicationCommandOptionTypes.SubCommand) {
if (optionType === ApplicationCommandOptionTypes.SubCommand) {
// Check if command has subcommand and handle types
if (!cmd.subcommands) return;
if (!command?.subcommands) return
// Try to find the command
const found = cmd.subcommands.find((cmd) => cmd.name == interaction.data?.options?.[0].name);
if (!found) return;
const found = command.subcommands.find((command) => command.name === interaction.data?.options?.[0].name)
if (!found) return
if (isSubCommandGroup(found)) return;
if (isSubCommandGroup(found)) return
cmd = found;
cmdName += ` ${cmd?.name}`;
command = found
}
if (!cmd) return;
}
// Get options
const options =
interaction.data?.options?.[0].type === DiscordApplicationCommandOptionTypes.SubCommandGroup
? interaction.data?.options?.[0].options?.[0].options
: interaction.data?.options?.[0].type === DiscordApplicationCommandOptionTypes.SubCommand
? interaction.data?.options[0].options
: interaction.data?.options;
try {
if (!command) throw new Error('Not command could be found')
// Prepare info for logs
const user = member || interaction.user;
const guild = interaction.guildId
? await customCacheHandlers.get("guilds", snowflakeToBigint(interaction.guildId))
: undefined;
await command.execute(interaction)
// Log cmd trigger
logSlashCommand(cmdName, user, guild, "trigger");
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})` : ``}`,
)
// Check inhibitors
for await (const inhibitor of bot.inhibitors.values()) {
const inhibited = await inhibitor(cmd, interaction, member);
if (inhibited) {
// Log cmd inhibition
logSlashCommand(cmdName, user, guild, "inhibited");
return;
}
}
if (typeof err !== 'object' || !err || !hasProperty(err, 'message') || err.message === 'Not command could be found') return
// Check if command has execute
if (!cmd.execute) {
logger.error(`Command ${cmdName} is missing execute.`);
sendBasicResponse(data.id, data.token, "This command is not configured to be executed.");
return;
}
// Get Command context
const cmdCtx = createCommandCtx(interaction, undefined, undefined, options, member);
// Execute command
let err;
try {
if (cmd.longExecution)
// Thinking....
await sendInteractionResponse(interaction.id, interaction.token, {
type: DiscordInteractionResponseTypes.DeferredChannelMessageWithSource,
});
await cmd.execute(cmdCtx);
} catch (error) {
err = true;
// Log command fail
logSlashCommand(cmdName, user, guild, "failed", error);
// Log the error
logger.error(`Error ${cmdName}`, error);
// Send error message to user
sendInteractionResponse(interaction.id, interaction.token, {
type: DiscordInteractionResponseTypes.ChannelMessageWithSource,
data: {
embeds: [
createSimpleEmbed("error")
.setDescription("Unexpected Error")
.setFooter("This error has been reported to the developers of this bot."),
],
},
}).catch(logger.error);
}
// Log success
if (!err) logSlashCommand(cmdName, user, guild, "success");
},
});
*/
logger.error(err)
}
}

View File

@@ -1,3 +0,0 @@
import type { EventHandlers } from '../../deps.ts.js'
export const events: Partial<EventHandlers> = {}

View File

@@ -1,8 +1,22 @@
import { events } from './mod.ts.js'
import { logger } from '../utils/logger.ts.js'
import { ActivityTypes } from '@discordeno/bot'
import { bot } from '../bot.js'
import { createLogger } from '../utils/logger.js'
const log = logger({ name: 'Event: Ready' })
const logger = createLogger({ name: 'Event: Ready' })
events.ready = () => {
log.info('Bot Ready')
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(),
},
},
],
})
}

View File

@@ -0,0 +1,19 @@
import 'dotenv/config'
import { bot } from './bot.js'
import { updateCommands } from './utils/helpers.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')
logger.info('Updating commands...')
await updateCommands()
await bot.start()

View File

@@ -1,13 +1,12 @@
import type { Bot, BotWithCache, CreateApplicationCommand, Guild, MakeRequired } from '../../deps.ts.js'
import { getGuild, hasProperty, upsertGuildApplicationCommands } from '../../deps.ts.js'
import { logger } from './logger.ts.js'
import type { subCommand, subCommandGroup } from '../commands/mod.ts.js'
import { commands } from '../commands/mod.ts.js'
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 log = logger({ name: 'Helpers' })
const logger = createLogger({ name: 'Helpers' })
/** This function will update all commands, or the defined scope */
export async function updateCommands(bot: BotWithCache, scope?: 'Guild' | 'Global') {
export async function updateCommands(scope?: 'Guild' | 'Global'): Promise<void> {
const globalCommands: Array<MakeRequired<CreateApplicationCommand, 'name'>> = []
const perGuildCommands: Array<MakeRequired<CreateApplicationCommand, 'name'>> = []
@@ -39,19 +38,21 @@ export async function updateCommands(bot: BotWithCache, scope?: 'Guild' | 'Globa
}
if (globalCommands.length && (scope === 'Global' || scope === undefined)) {
log.info('Updating Global Commands, changes should apply in short...')
await bot.helpers.upsertGlobalApplicationCommands(globalCommands).catch(log.error)
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 bot.guilds.forEach(async (guild: Guild) => {
await upsertGuildApplicationCommands(bot, guild.id, perGuildCommands)
})
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) {
export async function updateGuildCommands(bot: Bot, guild: Guild): Promise<void> {
const perGuildCommands: Array<MakeRequired<CreateApplicationCommand, 'name'>> = []
for (const command of commands.values()) {
@@ -68,32 +69,19 @@ export async function updateGuildCommands(bot: Bot, guild: Guild) {
}
if (perGuildCommands.length) {
await upsertGuildApplicationCommands(bot, guild.id, perGuildCommands)
await bot.helpers.upsertGuildApplicationCommands(guild.id, perGuildCommands)
}
}
export async function getGuildFromId(bot: BotWithCache, guildId: bigint): Promise<Guild> {
let returnValue: Guild = {} as Guild
export async function getGuildFromId(guildId: bigint): Promise<Guild> {
const cached = await bot.cache.guilds.get(guildId)
if (guildId !== 0n) {
if (bot.guilds.get(guildId)) {
returnValue = bot.guilds.get(guildId) as Guild
}
if (cached) return cached
await getGuild(bot, guildId).then((guild) => {
if (guild) bot.guilds.set(guildId, guild)
if (guild) returnValue = guild
})
}
return returnValue
return await bot.helpers.getGuild(guildId)
}
export function snowflakeToTimestamp(id: bigint) {
return Number(id / 4194304n + 1420070400000n)
}
export function humanizeMilliseconds(milliseconds: number) {
export function humanizeMilliseconds(milliseconds: number): string {
// Gets ms into seconds
const time = milliseconds / 1000
if (time < 1) return '1s'
@@ -111,10 +99,14 @@ export function humanizeMilliseconds(milliseconds: number) {
return `${dayString}${hourString}${minuteString}${secondString}`
}
export function isSubCommand(data: subCommand | subCommandGroup): data is subCommand {
export function isSubCommand(data: SubCommand | SubCommandGroup): data is SubCommand {
return !hasProperty(data, 'subCommands')
}
export function isSubCommandGroup(data: subCommand | subCommandGroup): data is subCommandGroup {
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]
}

View File

@@ -0,0 +1,15 @@
import { readdir } from 'node:fs/promises'
import logger from './logger.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://` 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),
)
}
}

View File

@@ -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,4 +92,15 @@ export function logger({
}
}
export const log = logger()
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
}

View 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/minimal/yarn.lock Normal file

File diff suppressed because it is too large Load Diff