@sodacore/discord
The @sodacore/discord
package provides a programmable Discord bot, and built-in OAuth2 features for SSO with Discord.
Installation
To install the plugin, simply install it via bun:
bun install @sodacore/discord
And then within your main.ts
file you can use the plugin like so:
import { Application } from '@sodacore/core';
import DiscordPlugin from '@sodacore/discord';
const app = new Application();
app.use(new DiscordPlugin({
// Options...
}));
app.start().catch(console.error);
Configuration
The Discord plugin has the following configuration options:
export type IConfig = {
token?: string, // The bot token to use, if not using OAuth2.
clientId?: string, // The client ID for OAuth2.
clientSecret?: string, // The client secret for OAuth2.
baseUrl?: string, // The base URL for the application.
scopes?: string[], // The OAuth2 scopes to request.
guildId?: string, // The ID of the guild to use for local registration of commands.
clientOptions?: ClientOptions, // Additional client options to pass to the Discord.js client.
events?: Array<keyof ClientEventTypes>, // The events to listen to, i.e. `messageCreate`.
};
Commands
There are are two ways to create slash commands;
@Command()
decorator
You can create commands using the @Command()
decorator like so:
import { Command, On, SlashCommandBuilder } from '@sodacore/discord';
@Command(
new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with Pong!'),
)
export class PingCommand {
@On.Command()
public async execute() {
return 'Pong!';
}
}
@SlashCommand
decorator.
This method is a bit more inline with the way controllers work and keeps everything a bit more decorator-based instead of a huge builder at the top.
WARNING
Be aware this is still experimental, so please if you intend to use it, please provide feedback.
import { Option, SlashCommand, StringOption, SubCommand, UserOption } from '@sodacore/discord';
@SlashCommand({ name: 'greet', description: 'Collection of greetings!' })
export class GreetingsCommand {
@SubCommand({ name: 'hello', description: 'Send a greeting message.' })
@UserOption({ name: 'friend', description: 'Choose your friend.' })
@StringOption({ name: 'message', description: 'Your message to your friend.' })
public async hello(
@Option('message') message?: string,
@Option('friend') friend?: number,
) {
return `${message || 'Hello'}, <@${friend}>!`;
}
}
IMPORTANT
When using the @SlashCommand
decorator, you do NOT need to use the @On...
decorators for the command/subcommands as the @SubCommand
decorator handles that for you. Although you will still need to use @On...
decorators for autocomplete, button and other style interactions.
@On.
decorators
When using the @Command()
decorator method, you can use the @On.
decorators to listen to different events, such as:
@On.Command()
- Listens for the root command execution.@On.SubCommand('subcommandName')
- Listens for a specific subcommand execution.@On.Button(uniqueId: string)
- Listens for button interactions with the specifically given unique ID.@On.SelectMenu(uniqueId: string)
- Listens for select menu interactions with the specifically given unique ID.@On.ModalSubmit(uniqueId: string)
- Listens for modal submit interactions with the specifically given unique ID.@On.Autocomplete(optionName: string, subCommand?: string)
- Listens for autocomplete interactions for a specific option, optionally within a specific subcommand or for all subcommands within the class.@On.Event(eventName: keyof ClientEventTypes)
- Listens for a specific Discord.js client event, such asmessageCreate
.
IMPORTANT
Any unique IDs prefixed with internal:...
are ignored from routing, as they are expected to be listened to directly. By default a unique ID will be looked for when routing, to find the matching controller and method to execute.
These decorators can be added to methods within your command class to deal with the various interactions.
Parameter decorators
Like the HTTP plugin, the Discord plugin also offers parameter decorators to extract data from the interaction, such as:
@Interaction()
- Injects the interaction object that triggered the command, this will be typed depending on the type of interaction, e.g.ChatInputCommandInteraction
for slash commands.@User()
- Injects the user that triggered the interaction, typed asUser
fromdiscord.js
.@Guild()
- Injects the guild (server) where the interaction was triggered, typed asGuild
fromdiscord.js
, ornull
if in a DM.@Channel()
- Injects the channel where the interaction was triggered, typed asGuildChannel | DMChannel | NewsChannel | ThreadChannel
fromdiscord.js
.@Client()
- Injects the Discord.js client instance, typed asClient
fromdiscord.js
.@Query()
- Injects the@On.Autocomplete()
query string.@Option(name: string)
- Injects the value of a specific option from the command, typed asstring | number | boolean | User | GuildMember | Role | Channel | Attachment | null
depending on the option type.@Field(name: string)
- Injects the value of a specific field from a modal submit interaction, typed asstring | null
.
As you can see these decorators allow you to easily extract the data you need from the interaction without having to manually parse it yourself, or if you want to access the raw interaction object, you can do that too via the @Interaction()
decorator.
Context menu commands
If you want to add context menu commands, (if you right-click a user or message in Discord), you can do so like this:
import { ContextMenu, Add } from '@sodacore/discord';
@ContextMenu()
export class MyContextMenus {
@Add.User('Greet User')
public async greetUser(
@Interaction() interaction: ContextMenuCommandInteraction,
@User() user: User,
) {
return `Hello, ${user.username}!`;
}
@Add.Message('Quote Message')
public async quoteMessage(
@Interaction() interaction: ContextMenuCommandInteraction,
@Channel() channel: TextChannel,
) {
const message = await channel.messages.fetch(interaction.targetId);
return `> ${message.content}\n- ${message.author.username}`;
}
}
Events
If you want to listen to an event, or create a collection of event listeners in one class, you can do so like:
import { Event, On } from '@sodacore/discord';
@Event()
export class MyEvents {
@On.Event('messageCreate')
public async onMessageCreate(
@Interaction() message: Message,
) {
if (message.content === 'ping') {
message.channel.send('Pong!');
}
}
}
Handlers
If you want to create a generic handler that might be a generic location for modal submissions for example.
import { Handler, On } from '@sodacore/discord';
@Handler()
export class MyHandlers {
@On.ModalSubmit('myModal')
public async onMyModalSubmit(
@Interaction() interaction: ModalSubmitInteraction,
@Field('inputField') inputField: string,
) {
await interaction.reply(`You submitted: ${inputField}`);
}
}
Authentication
The Discord plugin provides a series of authentication decorators to deal with command access control, such as:
@Auth.HasRole(roleId: string)
- Ensures the user has a specific role in the guild.@Auth.HasRoles(roleIds: string[])
- Ensures the user has all of the specified roles in the guild.@Auth.UserCustom(callback: IAuthFunctionUser)
- Passes a custom callback with the user as the parameter, expects a boolean return value.@Auth.GuildMemberCustom(callback: IAuthFunctionGuildMember)
- Passes a custom callback with the guild as the parameter, expects a boolean return value.@Auth.IsDirectMessage()
- Ensures the command is being run in a DM (Direct Message).@Auth.IsGuildMessage()
- Ensures the command is being run in a guild (server).
Types
export type IAuthFunctionUser = (user: User) => boolean | Promise<boolean>;
export type IAuthFunctionGuildMember = (user: GuildMember) => boolean | Promise<boolean>;
export type IAuthFunction = (user: User | GuildMember) => boolean | Promise<boolean>;
SlashCommandsProvider
The SlashCommandsProvider
is a built-in provider that can be used to register, unregister global or guild-specific slash commands. The provider is used by the built-in CLI commands, but you can also use it programmatically like so:
import { Controller, Get } from '@sodacore/http';
import { SlashCommandsProvider } from '@sodacore/discord';
import { Inject } from '@sodacore/di';
@Controller('/discord')
export class DiscordController {
@Inject() private slashCommands!: SlashCommandsProvider;
@Get('/register')
public async registerCommands() {
await this.slashCommands.registerGlobal();
}
}
The available methods are:
registerGlobal()
- Registers all global commands.registerGuild(guildId: string)
- Registers all commands for a specific guild.unregisterGlobal()
- Unregisters all global commands.unregisterGuild(guildId: string)
- Unregisters all commands for agetCommands()
- Gets all known commands from the application (in code, not from the API).
Types
export type IPromptsQuestionOptions = {
timeout?: number,
description?: string,
ephemeral?: boolean,
timestamp?: boolean,
fields?: APIEmbedField[],
};
export type IPromptsConfirmOptions = IPromptsQuestionOptions & {
acceptLabel?: string,
rejectLabel?: string,
};
export type IPromptsChoiceOptions = IPromptsQuestionOptions & {
placeholder?: string,
};
Scripts
The Discord plugin also provides a series of built-in scripts to help manage your bot, such as:
discord:commands:register
- Registers all commands, either globally or for a specific guild.discord:commands:unregister
- Unregisters all commands, either globally or for a specific guild.
Discord OAuth2
The Discord plugin provides built-in OAuth2 support, allowing you to authenticate users via Discord, and retrieve their basic profile information.
To use Discord OAuth2, you will need to configure the plugin with your Discord application's client ID, client secret and redirect URI.
You can import it like so:
import { Controller, Get } from '@sodacore/http';
import { OAuthProvider } from '@sodacore/discord';
@Controller('/auth/discord')
export class DiscordAuthController {
@Inject() private oauth!: OAuthProvider;
@Get('/login')
public async login() {
const redirectUrl = '<yourDomain>/auth/discord/accept';
return this.oauth.doAuthorisation(redirectUrl);
}
@Get('/accept')
public async accept(
@Query() query: URLSearchParams,
) {
const redirectUrl = '<yourDomain>/auth/discord/accept';
const data = this.oauth.doAccept(query, redirectUrl);
console.log(data);
}
}
You can then use the built-in authentication decorators to protect your routes, such as:
doAuthorisation(redirectUri: string, state?: string): Response
- Generates the Discord OAuth2 authorization URL to redirect the user to.doAccept(query: URLSearchParams, redirectUri: string): Promise<ITokenResult>
- Accepts the OAuth2 callback and retrieves the access token and user information.refreshToken(refreshToken: string): Promise<ITokenResult>
- Refreshes the access token using the provided refresh token.getUserM<T = Record<string, any>>(accessToken: string): Promise<T>
- Retrieves the user's Discord profile information using the access token.getCustom(apiPath: string, accessToken: string): Promise<any>
- Makes a custom API request to the Discord API using the access token.canUseSso(): boolean
- Checks if SSO can be used (i.e. if client ID and secret are configured).getUrl(redirectUri: string, state?: string): string
- Gets the OAuth2 authorization URL.
TIP
We suggest setting a script up to automate refreshing tokens for you, the return will tell you when the token expires, so you can store it and refresh it as needed, we do not handle this automatically.
Types
export type ITokenResult = {
tokenType: string,
expiresIn: number,
refreshToken: string,
accessToken: string,
scope: string,
};