Ledis
It's a typescript framework for building discord bots by decorators, based on discord.js, like discord.ts or nest.js
Index
Installation
Install ledis, discord.js and reflect-metadata to your project
npm i ledis discord.js reflect-metadata
Import reflect-metadata on top of your root file
import 'reflect-metadata';
Activate experimental decorators in your tsconfig.json
{
"compilerOptions": {
"emitDecoratorMetadata": true,
"experimentalDecorators": true
}
}
Usage
import {
bootstrap,
App,
Command,
CommandError,
CommandNotFound,
ErrorArg,
Once,
Module,
MatchGroup
} from 'ledis';
import { Client } from 'discord.js';
const token = 'Your token';
@Module()
class HiModule {
@Command('hi :id')
public hi(@MatchGroup('id') id: string){
return `hi ${id}`;
}
}
@App({
prefix: '!',
modules: [HiModule]
})
class Application {
constructor(private client: Client){}
@Once('ready')
public readyHandler() {
console.log(`Logged in as ${this.client?.user?.tag}!`);
}
@CommandError()
public commandError(@ErrorArg() error: Error) {
console.error(error);
}
@CommandNotFound()
public commandNotFound() {
return 'command not found!';
}
}
const client = new Client();
const main = async () => {
await bootstrap(client, Application);
await client.login(token);
};
main();
Example
Repository with example ledis discord bot
Dependency Injection
You can inject your dependencies to ledis App and Modules. For this define dependencies in @App/@Module dependencies field. Ledis have module scope support. If your local dependency can't find self dependencies in module scope then it will search this dependency in global scope, if global scope doesn't have this dependency or ledis di finds circular dependency then ledis di throws error. For add dependencies to global scope just add it to @App dependencies field.
import { Service, Module, App, Inject, Command, bootstrap } from 'ledis';
import { Client } from 'discord.js';
type LoggerData = 'loggerData';
const loggerData: LoggerData = 'loggerData';
const createLogger = (loggerData: LoggerData) => {
console.log(loggerData);
return console.log;
};
type TestValue = 'test';
const testValue: TestValue = 'test';
@Service()
class HiService {
public hi(){ return 'hi' }
}
@Service()
class TestService {
constructor(
@Inject('testValue')
private testValue: TestValue,
@Inject('logger')
private logger: (...args: any[]) => void
){}
public getTestValue(){
this.logger('test value getted!');
return this.testValue
}
}
@Module({
dependencies: [HiService, TestService]
})
class HiModule {
constructor(
private hiService: HiService,
private testService: TestService
){}
@Command('hi')
public hi(){ return this.hiService.hi() }
@Command('test')
public test(){ return this.testService.getTestValue() }
}
@App({
modules: [HiModule],
dependencies: [
{
token: 'testValue',
useValue: testValue,
},
{
token: 'loggerData',
useFactory: () => loggerData
},
{
token: 'logger',
useFactory: (loggerData: LoggerData) => createLogger(loggerData),
inject: ['loggerData']
}
]
})
class Application {}
const client = new Client();
const main = async () => {
await bootstrap(client, Application);
await client.login('token')
}
main();
Api
Decorators
@App(AppMetadata)
Define class as root application.
type AppMetadata = {
prefix?: string | ({ message: DiscordMessage }) => Promise<string>
modules?: Class[]
dependencies?: Dependencies
}
@App({}: AppMetadata)
class Application {}
@Module(ModuleMetadata)
Define class as module with commands. @Module class creates module scoped dependencies container for this module and services inside dependencies array.
type ModuleMetadata = {
dependencies?: Dependencies
}
const logger = console.log;
const loggerData = '123';
type Logger = typeof logger;
type LoggerData = typeof loggerData;
@Service()
class HiService {
constructor(
@Inject('logger')
private logger: Logger
){}
public hi(){
this.logger('run hi');
}
}
@Module({
dependencies: [
HiService,
{
token: 'loggerData',
useValue: loggerData
},
{
token: 'logger',
useFactory: (loggerData: LoggerData) => logger,
inject: ['loggerData']
}
]
})
class HiModule {
constructor(private hiService: HiService){}
@Command('hi')
public hi(){
return this.hiService.hi();
}
}
@Service()
Define class as ledis service. In @Service class ledis can insert dependencies.
@Service()
class HiService {
public hi() {
return 'hi';
}
}
@Service()
class HiUserService {
constructor(private hiService: HiService){}
public hi(){
return `${this.hiService.hi()} user!`;
}
}
@App({
dependencies: [HiService, HiUserService]
})
class Application {
constructor(private hiUserService: HiUserService){}
@Command('hi-user-command')
public hiUserCommand(){
return this.hiUserService.hi();
}
}
@Command(patternOrCommandMeta: string | RegExp | CommandMeta)
Define method as command handler.
If return not undefined, send this value to channel (message.channel.send(returnedValue))
@App()
class Application {
@Command('hi')
public hi(){
return `hi!`;
}
@Command(/hi/su)
public hiRegExp(){
return `hi!`;
}
@Command({
pattern: 'hi :id',
description: 'hi command'
data: {}
})
public hiId(
@MatchGroup('id') id: string;
){
return `hi ${id}!`;
}
}
@ComandError()
Define method as command error handler
If return not undefined, send this value to channel (message.channel.send(returnedValue))
@App()
class Application {
@CommandError()
public commandErrorHandler(
@ErrorArg() error: Error
){
console.log(error);
}
}
@CommandNotFound()
Define method as command not found handler
If return not undefined, send this value to channel (message.channel.send(returnedValue))
@App()
class Application {
@CommandNotFound()
public commandNotFoundHandler(
@Message() message: DiscordMessage
){
console.log(message.content);
}
}
@Middleware(handler: (ctx, next) => void)
Define a middleware for command/commandError/commandNotFound/on/once handler
@App()
class Application {
@Middleware(async (ctx, next) => next())
@Middleware((ctx, next) => {
if (context.message.content !== '!hi') return;
next();
})
@Command('hi')
public hi(){
return 'hi!'
}
}
@On(eventName: string)
Define on discord.js handler
@App()
class Application {
@On('message')
public messageHandler(
@Arguments() [message]: ArgumentsOf<"message">
){
console.log(message);
}
}
@Once(eventName: string)
Define once discord.js handler
import { Client } from 'discord.js';
@App()
class Application {
constructor(private client: Client){}
@Once('ready')
public readyHandler(){
console.log(`Logged in as ${this.client?.user?.tag}!`);
}
}
@Inject(token: string)
Define constructor parameter as injected token dependency
const logger = console.log;
type Logger = typeof logger;
@App({
dependencies: [{
token: 'logger',
useValue: logger
}]
})
class Application {
constructor(
@Inject('logger') logger: Logger
){}
@On('ready')
public readyHandler(){
this.logger('ready');
}
}
@Arg(argIndex: number)
Define parameter as command argument (prefixcommand arg1 arg2 arg3).
@App()
class Application {
@Command(/hi/su)
public hi(
@Arg(0) id: string
){
return `hi ${id}!`;
}
}
@MatchGroup(groupKey: string)
Define parameter as regexp match group.
@App()
class Application {
@Command('command :id')
public command(@MatchGroup('id') id: string) {
return id;
}
@Command('command2 ...ids')
public command2(@MatchGroup('ids') ids: string) {
return ids;
}
@Command(/command3 (?<value>.+)/su)
public command3(@MatchGroup('value') value: string) {
return value;
}
}
@Arguments()
Define parameter as discord.js on/once handler arguments
@App()
class Application {
@On('message')
public messageHandler(
@Arguments() [message]: ArgumentsOf<"message">
){
console.log(message);
}
}
@Commands()
Define parameter as LedisCommands
@App()
class Application {
@Command('help')
public help(
@Commands() commands: LedisCommands
){
const helpMessage = commands.map(...);
return helpMessage;
}
}
@ErrorArg()
Define parameter as command error
@App()
class Application {
@CommandError()
public commandErrorHandler(
@ErrorArg() error: Error
){
console.log(error);
}
}
@Message()
Define parameter as discord.js message
@App()
class Application {
@Command('hi :id')
public hi(
@Arg('id') id: string
@Message() message: DiscordMessage
){
message.reply(`hi ${id}!`);
}
}
Customize
Custom parameter decorator
You can create custom parameter decorator by createParamDecorator.
import { createParamDecorator, CommandParameterContext } from 'ledis';
const supportedTypes = [
'ledis:command-not-found',
'ledis:command-error',
'ledis:command',
] as const;
const MessageAuthor = (): ParameterDecorator => createParamDecorator(ctx => {
if (!supportedTypes.some((event) => event === ctx.event)) return;
return (ctx as CommandParameterContext).message.author;
})
Bootstrap
bootstrap(client: Client, App: Class) => Promise
Function for bootstrap your Application
import { Client } from 'discord.js';
import { bootstrap, App } from 'ledis';
@App()
class Application {}
const main = async () => {
const client = new Client();
await bootstrap(client, Application);
await client.login('your token');
};
main();