A progressive Node.js framework for building efficient and scalable server-side applications, heavily inspired by Angular.
NestJS package for discord.js
$ npm install discord-nestjs discord.js
Or via yarn
$ yarn add discord-nestjs discord.js
The module declaration proceeds in the same way as it is done in NestJS by means
creating a dynamic module through the forRoot
and forRootAsync
functions.
token
* - Your discord bot token. You can get herecommandPrefix
* - Global prefix for commandallowGuilds
- List of Guild IDs that the bot is allowed to work withdenyGuilds
- List of Guild IDs that the bot is not allowed to work withallowCommands
- Binding channels and user to a command. Can be overridden in the handler decoratorname
* - Command namechannels
- List of channel IDs with which the command will workusers
- List of user IDs with which the command will work
webhook
- Connecting with webhookwebhookId
* - Webhook idwebhookToken
* - Webhook token
usePipes
- List of pipes that will be applied to all handlers with the@Content
decorator (with class type other than string type). Can be overridden via the@UsePipes
decoratoruseGuards
- A list of guards that will apply to all handlers. Can be overridden via the@UseGuards
decorator- You can also set all options as for the client from the "discord.js" library
TransformPipe
and ValidationPipe
from discord-nestjs
package
Below is an example of creating a dynamic module using the forRoot
function
/*bot.module.ts*/
import { Module } from '@nestjs/common';
import { DiscordModule, TransformPipe, ValidationPipe } from 'discord-nestjs';
import { BotGateway } from './bot-gateway';
@Module({
imports: [
DiscordModule.forRoot({
token: 'Njg2MzI2OTMwNTg4NTY1NTQx.XmVlww.EF_bMXRvYgMUCQhg_jYnieoBW-k',
commandPrefix: '!',
allowGuilds: ['745366351929016363'],
denyGuilds: ['520622812742811698'],
allowCommands: [
{
name: 'some',
channels: ['745366352386326572'],
users: ['261863053329563648'],
channelType: ['dm']
},
],
webhook: {
webhookId: 'your_webhook_id',
webhookToken: 'your_webhook_token',
},
usePipes: [TransformPipe, ValidationPipe],
// and other discord options
}),
],
providers: [BotGateway],
})
export class BotModule {}
Or via the forRootAsync
function
/*bot.module.ts*/
import { Module } from '@nestjs/common';
import { DiscordModule, TransformPipe, ValidationPipe } from 'discord-nestjs';
import { BotGateway } from './bot-gateway';
@Module({
imports: [
DiscordModule.forRootAsync({
useFactory: () => ({
token: 'Njg2MzI2OTMwNTg4NTY1NTQx.XmVlww.EF_bMXRvYgMUCQhg_jYnieoBW-k',
commandPrefix: '!',
allowGuilds: ['745366351929016363'],
denyGuilds: ['520622812742811698'],
allowCommands: [
{
name: 'some',
channels: ['745366352386326572'],
users: ['261863053329563648'],
channelType: ['dm']
},
],
webhook: {
webhookId: 'your_webhook_id',
webhookToken: 'your_webhook_token',
},
usePipes: [TransformPipe, ValidationPipe],
// and other discord options
}),
}),
],
providers: [BotGateway],
})
export class BotModule {}
Alternatively, you can use the useClass
syntax
/*bot.module.ts*/
import { Module } from '@nestjs/common';
import { DiscordConfigService } from './discord-config-service';
import { BotGateway } from './bot-gateway';
import { DiscordModule } from 'discord-nestjs';
@Module({
imports: [
DiscordModule.forRootAsync({
useClass: DiscordConfigService
}),
],
providers: [BotGateway],
})
export class BotModule {}
You need to implement the DiscordOptionsFactory
interface
/*discord-config-service.ts*/
import { Injectable } from '@nestjs/common';
import { DiscordModuleOption, DiscordOptionsFactory, TransformPipe, ValidationPipe } from 'discord-nestjs';
@Injectable()
export class DiscordConfigService implements DiscordOptionsFactory {
createDiscordOptions(): DiscordModuleOption {
return {
token: 'Njg2MzI2OTMwNTg4NTY1NTQx.XmVlww.EF_bMXRvYgMUCQhg_jYnieoBW-k',
commandPrefix: '!',
allowGuilds: ['745366351929016363'],
denyGuilds: ['520622812742811698'],
allowCommands: [
{
name: 'some',
channels: ['745366352386326572'],
users: ['261863053329563648'],
channelType: ['dm']
},
],
webhook: {
webhookId: 'your_webhook_id',
webhookToken: 'your_webhook_token',
},
usePipes: [TransformPipe, ValidationPipe],
// and other discord options
}
}
}
Create a class (for example BotGateway
) and mark it with the decorator @Injectable
or @Controller
.
You can get the client provider by adding a variable of type DiscordClientProvider
to the dependency.
Below is an example of how you can notify yourself that a bot has successfully established a connection to the Discord API.
/*bot.gateway.ts*/
import { Injectable, Logger } from '@nestjs/common';
import { Once, DiscordClientProvider } from 'discord-nestjs';
@Injectable()
export class BotGateway {
private readonly logger = new Logger(BotGateway.name);
constructor(private readonly discordProvider: DiscordClientProvider) {}
@Once({ event: 'ready' })
onReady(): void {
this.logger.log(`Logged in as ${this.discordProvider.getClient().user.tag}!`);
this.discordProvider.getWebhookClient().send('hello bot is up!');
}
}
You can also get the client provider using the @Client
decorator.
/*bot.gateway.ts*/
import { Injectable, Logger } from '@nestjs/common';
import { Once, ClientProvider } from 'discord-nestjs';
@Injectable()
export class BotGateway {
private readonly logger = new Logger(BotGateway.name);
@Client()
discordProvider: ClientProvider;
@Once({ event: 'ready' })
onReady(): void {
this.logger.log(`Logged in as ${this.discordProvider.getClient().user.tag}!`);
}
}
Use the @OnCommand
decorator to declare a command handler.
An example of creating a command is shown below. By default, the handler arguments will be the same as if you were signing up for an event in the "discord.js" library. (hint)
The OnCommand
decorator always subscribes to the "message" event
/*bot.gateway.ts*/
import { Injectable } from '@nestjs/common';
import { OnCommand } from 'discord-nestjs';
import { Message } from 'discord.js';
@Injectable()
export class BotGateway {
@OnCommand({ name: 'start' })
async onCommand(message: Message): Promise<void> {
await message.reply(`Execute command: ${message.content}`);
}
}
Subscription to incoming events (hint)
Use the @On
decorator to subscribe to an event
/*bot.gateway.ts*/
import { Injectable } from '@nestjs/common';
import { On } from 'discord-nestjs';
import { Message } from 'discord.js';
@Injectable()
export class BotGateway {
@On({ event: 'message' })
async onMessage(message: Message): Promise<void> {
if (!message.author.bot) {
await message.reply("I'm watching you");
}
}
}
You can also subscribe to an event once using the @Once
decorator
/*bot.gateway.ts*/
import { Injectable } from '@nestjs/common';
import { Once } from 'discord-nestjs';
import { Message } from 'discord.js';
@Injectable()
export class BotGateway {
@Once({ event: 'message' })
async onceMessage(message: Message): Promise<void> {
if (!message.author.bot) {
await message.reply("I'm watching you");
}
}
}
By default, the library sets the handler arguments on its own, as it was said above,
but you can manipulate the arguments yourself using the @Content
and @Context
decorators
- Content - Message body (applicable only for "message" event)
- Context - Default handler arguments (hint)
/*bot.gateway.ts*/
import { Content, Context, OnCommand } from 'discord-nestjs';
import { Injectable, Logger } from '@nestjs/common';
import { Message } from 'discord.js';
@Injectable()
export class BotGateway {
private readonly logger = new Logger(BotGateway.name);
@OnCommand({ name: 'start' })
async onCommand(@Content() content: string, @Context() [context]: [Message]): Promise<void> {
await context.reply(`Execute command: ${content}`, `Args: ${context}`);
}
}
To intercept messages before invoking the handler, use the @UsePipes
decorator.
Works only with the "message" event.
For convenience, the library already has an implementation of TransformPipe
and ValidationPipe
.
First thing you need to do is create a DTO class.
First, each field in the class must be marked with the @Expose
decorator from the "class-transform" library,
secondly, it is necessary to mark how we will enter data into the variable.
Use the @ArgNum
and @ArgRange
decorators for markup.
@ArgNum
takes the value at the index. Think of @ArgRange
as a slice
function for arrays.
Then you can add validation as you like.
@UsePipes, TransformPipe, ValidationPipe
from the discord-nestjs
package
/*registration.dto.ts*/
import { ArgNum, ArgRange } from 'discord-nestjs';
import { Expose } from 'class-transformer';
import { IsNumber, Min, IsArray } from 'class-validator';
export class RegistrationDto {
@ArgRange(() => ({ formPosition: 1, toPosition: 4 }))
@Expose()
@IsArray()
name: string[];
@ArgNum((last: number) => ({ position: last }))
@Expose()
@Type(() => Number)
@IsNumber()
@Min(18)
age: number;
}
After that we attach the decorator @UsePipes
to the handler and pass TransformPipe
and ValidationPipe
to the arguments
in the same sequence.
Pipes are executed sequentially from left to right. The @UsePipes
declaration overrides the global usePipes
declaration.
/*bot.gateway.ts*/
import { UsePipes, Content, Context, OnCommand, TransformPipe, ValidationPipe } from 'discord-nestjs';
import { Injectable } from '@nestjs/common';
import { RegistrationDto } from './registration.dto';
import { Message } from 'discord.js';
@Injectable()
export class BotGateway {
@OnCommand({name: 'reg'})
@UsePipes(TransformPipe, ValidationPipe)
async onSomeEvent(
@Content() content: RegistrationDto,
@Context() [context]: [Message]
): Promise<void> {
await context.reply(
`User was created! Id: ${context.author.id}, FIO: ${content.name.join('-')}, Age: ${content.age}`
);
}
}
Input:
!reg Ivan Ivanovich Ivanov 22
Output:
User was created!: Id: 261863053329563648, Ivan-Ivanovich-Ivanov, Age: 22
You can also set @UsePipes
decorator on class. In this case, the decorator is applied to all methods in the class.
Transform alias to user class
/*user.dto.ts*/
import { ArgNum, TransformToUser } from 'discord-nestjs';
import { Expose } from 'class-transformer';
import { User } from 'discord.js';
export class UserDto {
@ArgNum((last: number) => ({ position: 1 }))
@Expose()
@TransformToUser()
user: User;
}
Create command handler
TransformPipe
required for transform input string to DTO.
You can also use ValidationPipe
for validate input
/*bot.gateway.ts*/
import { Message } from 'discord.js';
import { Content, Context, OnCommand, UsePipes } from 'discord-nestjs';
import { UserDto } from './user.dto';
import { TransformPipe } from 'discord-nestjs';
@Injectable()
export class BotGateway {
@OnCommand({ name: 'avatar' })
@UsePipes(TransformPipe)
async onCommand(@Content() content: UserDto, @Context() [context]: [Message]): Promise<void> {
await context.reply(`User avatar: ${content.user.avatarURL()}`);
}
}
Input:
!avatar @Федок
Output:
User avatar: https://cdn.discordapp.com/avatars/261863053329563648/d12c5a04be7bcabea7b9778b7e4fa6d5.webp
In order to override error handling in ValidationPipe
you need to create an instance of the class manually.
/*bot.gateway.ts*/
import { UsePipes, Content, Context, OnCommand, TransformPipe, ValidationPipe } from 'discord-nestjs';
import { Injectable } from '@nestjs/common';
import { RegistrationDto } from './registration.dto';
import { Message, MessageEmbed } from 'discord.js';
import { ValidationError } from 'class-validator';
@Injectable()
export class BotGateway {
@OnCommand({name: 'reg'})
@UsePipes(TransformPipe, new ValidationPipe({
exceptionFactory: (
errors: ValidationError[], message: Message
) => new MessageEmbed().setTitle('Upss!').setDescription(message.content)
}))
async onSomeEvent(
@Content() content: RegistrationDto,
@Context() [context]: [Message]
): Promise<void> {
await context.reply(
`User was created! Id: ${context.author.id}, FIO: ${content.name.join('-')}, Age: ${content.age}`
);
}
}
You can also create your own TransformPipe
or ValidationPipe
by implementing the DiscordPipeTransform
interface.
/*transform.pipe.ts*/
import { TransformProvider, ConstructorType, ValidationPipe, DiscordPipeTransform } from 'discord-nestjs';
import { ClientEvents, Message } from 'discord.js';
import { Injectable } from '@nestjs/common';
@Injectable()
export class TransformPipe implements DiscordPipeTransform {
constructor(
private readonly transformProvider: TransformProvider
) {
}
transform(
event: keyof ClientEvents,
[context]: [Message],
content?: any,
type?: ConstructorType<any>
): any {
return this.transformProvider.transformContent(type, content);
}
}
To protect commands and events, use. The canActive
function returns boolean. If one of the guards returns false,
then the chain will stop there and the handler itself will not be called.
The @UseGuards
declaration overrides the global useGuards
declaration.
@UseGuards
from the discord-nestjs
package
You need to implement the DiscordGuard
interface
/*bot.guard.ts*/
import { DiscordGuard } from 'discord-nestjs';
import { ClientEvents, MessageEmbed } from 'discord.js';
export class BotGuard implements DiscordGuard {
async canActive(
event: keyof ClientEvents,
[context]: [any],
): Promise<boolean> {
if (context.author.id === '766863033789563648') {
return true;
} else {
const embed = new MessageEmbed().setColor().setTitle('Ups! Not allowed!');
await context.reply(embed);
return false;
}
}
}
/*bot.gateway.ts*/
import { On, UseGuards, OnCommand } from 'discord-nestjs';
import { Message } from 'discord.js';
import { BotGuard } from './bot.guard';
import { Injectable } from '@nestjs/common';
@Injectable()
export class BotGateway {
@UseGuards(BotGuard)
@OnCommand({ name: 'hide' })
async guardCommand(message: Message): Promise<void> {
// to do something
}
}
You can also set @UseGuards
decorator on class. In this case, the decorator is applied to all methods in the class.
You can use a middleware to process all incoming messages.
To do this, you need to implement the DiscordMiddleware
interface.
/*bot.middleware.ts*/
import { Middleware, DiscordMiddleware } from 'discord-nestjs';
import { Logger } from '@nestjs/common';
import { ClientEvents } from 'discord.js';
@Middleware()
export class BotMiddleware implements DiscordMiddleware {
private readonly logger = new Logger(BotMiddleware.name);
use(
event: keyof ClientEvents,
context: any[],
): void {
if (event === 'message') {
this.logger.log('On message event triggered');
}
}
}
Also don't forget to add your middleware to the providers.
@Module({
providers: [BotMiddleware],
})
export class BotModule {}
Inject client provider
Mark as command handler
name
* - Command nameprefix
- Command prefix (If set, it overrides the global)isRemoveCommandName
- Remove command name from input string (Defaulttrue
)isRemovePrefix
- Remove prefix from input string (Defaulttrue
)isIgnoreBotMessage
- Ignore messages from bots (Defaulttrue
)allowChannels
- List of channel IDs with which the command will work (If set, it overrides the global)isRemoveMessage
- Remove message from channel after processing (Defaultfalse
)allowUsers
- List of user IDs with which the command will work (If set, it overrides the global)channelType
- Filter by text channel type (If set, it overrides the global)
Handle discord events hint
event
* - Name of the event to listen to
Handle discord events (only once) hint
event
* - Name of the event to listen to
Message content (applicable only for "message" event)
Default handler arguments (hint)
To intercept incoming messages for some function
- List of classes or instances that implement the
DiscordPipeTransform
interface
Transform alias to user class
Works only in conjunction with @ArgNum
and @ArgRange
decorator
throwError
- If an error occurs from Discord APIm it will be thrown (Defaultfalse
)
Set value by argument number
- arguments
last
- Last index position
- return
position
* - Position index form input
Set value by argument number
- arguments
last
- Last index position
- return
formPosition
* - Start index position form inputtoPosition
- Finish index position form input (default last index of input)
To guard incoming messages
- List of classes or instances that implement the
DiscordGuard
interface
For handling intermediate requests
allowEvents
- Handled eventsdenyEvents
- Skipped events
Any questions or suggestions? Discord Федок#3051