Codespaces →
Your fully configured development environment for this workshop
GitLab →
Where OpenBot collaboration and version control officially takes place
Slide deck →
Presentation slides for the OpenBot Workshop on January 15th
Cordette TypeDoc →
Detailed documentation of the classes and methods provided by the module library
Discord.js Guide →
Guide to using Discord.js, the underlying library supporting all interactions with Discord
Discord Dev Portal →
Documentation of bot capabilities and technical concepts
Inputs
Event handler
Whenever this happens, do that
mod.when(Events.MessageCreate, event => await event.react('✨'))
// When this happens, do that once and never again:
mod.once(Events.ClientReady, event => console.log('module started!'))
mod.when(Events.MessageCreate, event => await event.react('✨'))
// When this happens, do that once and never again:
mod.once(Events.ClientReady, event => console.log('module started!'))
Slash commands
Build a chat input command and validate/fulfill interactions
mod.slash('ping', 'Get a response from the bot',
async intx => await intx.reply('pong!'))
// Fully customized:
mod.slash('quote', 'Short description', {
build(builder) {
return builder
.addStringOption((opt) => opt
.setName("content")
.setDescription("The statement to quote")
.setRequired(true))
},
check(intx) {
if (intx.options.getString("content", true).trim().length === 0) {
throw new Error('The quote cannot be empty')
}
},
run(intx, resolved) {
/* fulfill the interaction to completion */
/* only called if "check" did not fail */
},
/* optional: limit this command to a given guild */
guild: '1002274815270465607'
})
mod.slash('ping', 'Get a response from the bot',
async intx => await intx.reply('pong!'))
// Fully customized:
mod.slash('quote', 'Short description', {
build(builder) {
return builder
.addStringOption((opt) => opt
.setName("content")
.setDescription("The statement to quote")
.setRequired(true))
},
check(intx) {
if (intx.options.getString("content", true).trim().length === 0) {
throw new Error('The quote cannot be empty')
}
},
run(intx, resolved) {
/* fulfill the interaction to completion */
/* only called if "check" did not fail */
},
/* optional: limit this command to a given guild */
guild: '1002274815270465607'
})
Context menu items
Attach a new context menu button to a message or a user, and do this when someone clicks it
mod.menu('Menu item label', ApplicationCommandType.User, intx => { /* what to do? */ })
mod.menu('Menu item label', ApplicationCommandType.Message, {
build(builder) {
/* customize the context menu item, e.g. by adding localized labels */
/* optional */
},
check(intx) {
/* validate the incoming interaction to see if it should be fulfilled or rejected */
/* optional */
/* return something resolved to pass it to "run" below */
/* throw an error to indicate an invalid interaction */
},
run(intx, resolved) {
/* fulfill the interaction to completion */
/* only called if "check" did not fail */
},
/* optional: limit this command to a given guild */
guild: '12345678901234567890'
})
mod.menu('Menu item label', ApplicationCommandType.User, intx => { /* what to do? */ })
mod.menu('Menu item label', ApplicationCommandType.Message, {
build(builder) {
/* customize the context menu item, e.g. by adding localized labels */
/* optional */
},
check(intx) {
/* validate the incoming interaction to see if it should be fulfilled or rejected */
/* optional */
/* return something resolved to pass it to "run" below */
/* throw an error to indicate an invalid interaction */
},
run(intx, resolved) {
/* fulfill the interaction to completion */
/* only called if "check" did not fail */
},
/* optional: limit this command to a given guild */
guild: '12345678901234567890'
})
Terms
❄️ Snowflake
Unique ID that represents an entity (message, user, channel, guild, etc.) on Discord. Example: 135824500603224064
Guild
Discord server (technical term for disambiguation)
Member vs User
A User is a human's Discord account (e.g. username#ID). A Member is a User's profile in a Guild (server profile, roles, etc.)
Command
Interactive feature provided by a bot. Types: Slash command, context menu command
Gateway Intent
Group of events that take place on Discord. Specify intents to receive their events from Discord
Queries
All guild members
List all the members and their profiles in a guild
const guild = await mod.client.guilds.resolve('1028759256750633062')!
const members = await guild.members.list({limit: 1000})
// members is a Collection<Snowflake, GuildMember>
const guild = await mod.client.guilds.resolve('1028759256750633062')!
const members = await guild.members.list({limit: 1000})
// members is a Collection<Snowflake, GuildMember>
Guild member & roles
List all the roles held by a guild member
const guild = await mod.client.guilds.resolve('1028759256750633062')!
const member = await guild.members.fetch({user: '135824500603224064'})
// member.roles.cache is a Collection<string, Role>
const guild = await mod.client.guilds.resolve('1028759256750633062')!
const member = await guild.members.fetch({user: '135824500603224064'})
// member.roles.cache is a Collection<string, Role>
Resolve user
Get a User by ID for DMs
const user = await mod.client.users.fetch('135824500603224064')
// then, IF you want to DM them
await user.send('sliding into your DMs ;)')
const user = await mod.client.users.fetch('135824500603224064')
// then, IF you want to DM them
await user.send('sliding into your DMs ;)')
Resolve channel
Get a channel by ID
const chan = await host.client.channels.fetch('1005324615503073360')
// then, IF you want to send something, reassure the type system that it's a text channel
if (chan?.isTextBased()) {
await chan.send('this is the channel!')
// IF you want to read the channel's history (requires the MessageContent privileged intent)
const history = await chan.messages.fetch({limit: 100})
}
const chan = await host.client.channels.fetch('1005324615503073360')
// then, IF you want to send something, reassure the type system that it's a text channel
if (chan?.isTextBased()) {
await chan.send('this is the channel!')
// IF you want to read the channel's history (requires the MessageContent privileged intent)
const history = await chan.messages.fetch({limit: 100})
}
Outputs
Plain-text reply
Reply to the user that invoked the slash command
async (intx: ChatInputCommandInteraction) => {
// everyone in the guild can see:
await intx.reply('sus?')
// only the user can see:
await intx.reply({
content: 'sus?',
ephemeral: true
})
// DM the user
await intx.user.send('sus?')
}
async (intx: ChatInputCommandInteraction) => {
// everyone in the guild can see:
await intx.reply('sus?')
// only the user can see:
await intx.reply({
content: 'sus?',
ephemeral: true
})
// DM the user
await intx.user.send('sus?')
}
Reactions
React to any given message (e.g. from MessageCreate)
async (msg: MessageEvent) => {
await msg.react('😎')
}
async (msg: MessageEvent) => {
await msg.react('😎')
}
Embeds and buttons
Display embedded cards and buttons for more interaction
async (intx: ChatInputCommandInteraction) => {
// Display a button "✅ Agree" under the message
const buttons = new ActionRowBuilder<ButtonBuilder>()
.addComponents(new ButtonBuilder()
.setCustomId('agree')
.setLabel('Agree')
.setStyle(ButtonStyle.Primary)
.setEmoji('✅'))
await intx.reply({
// Build and preview embeds at https://discohook.org
embeds: [{
title: 'Some title',
description: "Embeds are cool because humans can't send them legally"
}],
components: [buttons]
})
}
async (intx: ChatInputCommandInteraction) => {
// Display a button "✅ Agree" under the message
const buttons = new ActionRowBuilder<ButtonBuilder>()
.addComponents(new ButtonBuilder()
.setCustomId('agree')
.setLabel('Agree')
.setStyle(ButtonStyle.Primary)
.setEmoji('✅'))
await intx.reply({
// Build and preview embeds at https://discohook.org
embeds: [{
title: 'Some title',
description: "Embeds are cool because humans can't send them legally"
}],
components: [buttons]
})
}
Modals
Display a form in a focused dialog to the user
async (intx: CommandInteraction) => {
const modal = new ModalBuilder()
.setCustomId('form-modal')
.setTitle('My Modal')
const question = new TextInputBuilder()
.setCustomId('number-input')
.setLabel('Enter any significant number')
.setStyle(TextInputStyle.Short)
const firstRow = new ActionRowBuilder<ModalActionRowComponentBuilder>()
.addComponents(question)
modal.addComponents(firstRow)
await intx.showModal(modal)
}
async (intx: CommandInteraction) => {
const modal = new ModalBuilder()
.setCustomId('form-modal')
.setTitle('My Modal')
const question = new TextInputBuilder()
.setCustomId('number-input')
.setLabel('Enter any significant number')
.setStyle(TextInputStyle.Short)
const firstRow = new ActionRowBuilder<ModalActionRowComponentBuilder>()
.addComponents(question)
modal.addComponents(firstRow)
await intx.showModal(modal)
}
Roles
Add or remove a GuildMember role
async (intx: CommandInteraction) => {
if (intx.member !== null) {
// find the role ID in server settings, then ADD
await intx.member.roles?.add('1028749978870493234')
// OR remove
await intx.member.roles?.remove('1028749978870493234')
}
}
async (intx: CommandInteraction) => {
if (intx.member !== null) {
// find the role ID in server settings, then ADD
await intx.member.roles?.add('1028749978870493234')
// OR remove
await intx.member.roles?.remove('1028749978870493234')
}
}