mirror of
https://github.com/discordeno/discordeno.git
synced 2026-05-21 02:40:08 +00:00
docs: Reaction roles bot example (#3361)
* Update docusaurus typescript setup for v3 And fix lint-staged and eslint * Enable automatic JSX runtime * Remove babel config and dependencies * update yarn.lock * add typecheck to site workflow * update typedoc config * downgrade docusaurus packages * Update site.yml * Type context and options in webpack-docusaurus-plugin.ts * Add reaction-roles code from docs example * Finish /roles reactions create command, missing event handler * Add handler for the role buttons * Initial update to reactionroles.md + code changes accordingly * Finish reactionroles.md file * Corrections to reactionroles.md * update deps & add --strip-leading-paths to swc * Add a note for the possibile ratelimit on command upsert * Update website/docs/examples/reactionroles.md Co-authored-by: LTS20050703 <lts20050703@gmail.com> * Apply suggestions from code review Co-authored-by: LTS20050703 <lts20050703@gmail.com> * Update website/docs/examples/reactionroles.md Co-authored-by: LTS20050703 <lts20050703@gmail.com> * Use a register-commands.ts for app commands * Apply suggestions from code review Co-authored-by: LTS20050703 <lts20050703@gmail.com> * Update for the latest version of discordeno Also add the tsconfig for the reaction roles example as i forgot it and was using the root dir one * Update deps, add typescript as dev deps, add .swcrc --------- Co-authored-by: Matt Hatcher <3768988+MatthewSH@users.noreply.github.com> Co-authored-by: Jonathan Ho <heiheiho000@gmail.com> Co-authored-by: Skillz4Killz <23035000+Skillz4Killz@users.noreply.github.com> Co-authored-by: LTS20050703 <lts20050703@gmail.com>
This commit is contained in:
1
examples/reaction-roles/.env.example
Normal file
1
examples/reaction-roles/.env.example
Normal file
@@ -0,0 +1 @@
|
||||
TOKEN= # Your discord bot token
|
||||
35
examples/reaction-roles/.gitignore
vendored
Normal file
35
examples/reaction-roles/.gitignore
vendored
Normal file
@@ -0,0 +1,35 @@
|
||||
# dependencies
|
||||
node_modules
|
||||
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/sdks
|
||||
!.yarn/versions
|
||||
# testing
|
||||
coverage
|
||||
|
||||
# build
|
||||
dist
|
||||
|
||||
# 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
|
||||
24
examples/reaction-roles/.swcrc
Normal file
24
examples/reaction-roles/.swcrc
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"$schema": "https://json.schemastore.org/swcrc",
|
||||
"jsc": {
|
||||
"parser": {
|
||||
"syntax": "typescript",
|
||||
"decorators": true,
|
||||
"dynamicImport": true
|
||||
},
|
||||
"transform": {
|
||||
"legacyDecorator": true,
|
||||
"decoratorMetadata": true
|
||||
},
|
||||
"target": "es2022",
|
||||
"keepClassNames": true,
|
||||
"loose": true
|
||||
},
|
||||
"module": {
|
||||
"type": "es6",
|
||||
"strict": false,
|
||||
"strictMode": true,
|
||||
"lazy": false,
|
||||
"noInterop": false
|
||||
}
|
||||
}
|
||||
14
examples/reaction-roles/README.md
Normal file
14
examples/reaction-roles/README.md
Normal file
@@ -0,0 +1,14 @@
|
||||
# Reaction roles example bot
|
||||
|
||||
Example bot for reaction-roles using Discord Interactions and not classic reactions.
|
||||
|
||||
## Setup
|
||||
|
||||
1. Rename `.env.example` to `.env` and add your bot token in the `TOKEN` variable
|
||||
1. Replace `REPLACE WITH YOUR GUILD ID` in `src/register-commands.ts` with your test guild id
|
||||
|
||||
## Run the bot
|
||||
|
||||
1. `yarn` to install the dependencies
|
||||
1. `yarn build` to build the .ts files into .js
|
||||
1. `yarn start` (or `node ./dist/index.js`) to run the bot
|
||||
24
examples/reaction-roles/package.json
Normal file
24
examples/reaction-roles/package.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "reaction-roles",
|
||||
"description": "",
|
||||
"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",
|
||||
"dotenv": "^16.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@swc/cli": "^0.3.12",
|
||||
"@swc/core": "^1.5.25",
|
||||
"@types/node": "^20.14.2",
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
12
examples/reaction-roles/src/collector.ts
Normal file
12
examples/reaction-roles/src/collector.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { EventEmitter } from 'node:events'
|
||||
|
||||
// Extremely minimal collector class
|
||||
export default class ItemCollector<T> extends EventEmitter {
|
||||
onItem(callback: (item: T) => unknown): void {
|
||||
this.on('item', callback)
|
||||
}
|
||||
|
||||
collect(item: T): void {
|
||||
this.emit('item', item)
|
||||
}
|
||||
}
|
||||
12
examples/reaction-roles/src/commands/index.ts
Normal file
12
examples/reaction-roles/src/commands/index.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import type { Interaction } from '@discordeno/bot'
|
||||
import type { CreateSlashApplicationCommand } from '@discordeno/types'
|
||||
import roles from './roles.js'
|
||||
|
||||
export const commands = new Map<string, Command>([roles].map((cmd) => [cmd.name, cmd]))
|
||||
|
||||
export default commands
|
||||
|
||||
export interface Command extends CreateSlashApplicationCommand {
|
||||
/** Handler that will be executed when this command is triggered */
|
||||
execute: (interaction: Interaction, args: Record<string, unknown>) => Promise<unknown>
|
||||
}
|
||||
476
examples/reaction-roles/src/commands/roles.ts
Normal file
476
examples/reaction-roles/src/commands/roles.ts
Normal file
@@ -0,0 +1,476 @@
|
||||
import {
|
||||
DiscordInteractionContextType,
|
||||
MessageComponentTypes,
|
||||
TextStyles,
|
||||
type ActionRow,
|
||||
type ButtonComponent,
|
||||
type Interaction,
|
||||
type Role,
|
||||
type SelectMenuComponent,
|
||||
} from '@discordeno/bot'
|
||||
import { ApplicationCommandOptionTypes, ButtonStyles } from '@discordeno/types'
|
||||
import ItemCollector from '../collector.js'
|
||||
import { collectors } from '../events/interactionCreate.js'
|
||||
import { bot } from '../index.js'
|
||||
import type { Command } from './index.js'
|
||||
|
||||
const command: Command = {
|
||||
name: 'roles',
|
||||
description: 'Role management on your server.',
|
||||
// Do not allow to run this command in DM. Only in guilds.
|
||||
contexts: [DiscordInteractionContextType.Guild],
|
||||
// Require the user to have Manage Guild and Manage Roles by-default, server admins can override this setting for their server
|
||||
defaultMemberPermissions: ['MANAGE_GUILD', 'MANAGE_ROLES'],
|
||||
options: [
|
||||
{
|
||||
name: 'reactions',
|
||||
description: 'Manage the role reactions on your server.',
|
||||
type: ApplicationCommandOptionTypes.SubCommandGroup,
|
||||
options: [
|
||||
{
|
||||
name: 'create',
|
||||
description: 'Create a reaction role on your server.',
|
||||
type: ApplicationCommandOptionTypes.SubCommand,
|
||||
options: [
|
||||
{
|
||||
name: 'role',
|
||||
description: 'What role would you like to set for this button?',
|
||||
type: ApplicationCommandOptionTypes.Role,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'emoji',
|
||||
description: "What would you like to set as this button's emoji?",
|
||||
type: ApplicationCommandOptionTypes.String,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
name: 'color',
|
||||
description: "What color would you like to set as this button's color?",
|
||||
type: ApplicationCommandOptionTypes.Integer,
|
||||
required: true,
|
||||
choices: [
|
||||
{ name: 'Blue', value: ButtonStyles.Primary },
|
||||
{ name: 'Green', value: ButtonStyles.Success },
|
||||
{ name: 'Grey', value: ButtonStyles.Secondary },
|
||||
{ name: 'Red', value: ButtonStyles.Danger },
|
||||
],
|
||||
},
|
||||
{
|
||||
required: false,
|
||||
name: 'label',
|
||||
description: 'What would you like to set for the name on this button?',
|
||||
type: ApplicationCommandOptionTypes.String,
|
||||
// Discord imposed limit for button
|
||||
maxLength: 80,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
async execute(interaction, args: CommandArgs) {
|
||||
// Create a reaction role
|
||||
if (args.reactions?.create) {
|
||||
// Ensure that there is a channelId
|
||||
if (!interaction.channelId) {
|
||||
await interaction.respond('Could not get the current channel.', { isPrivate: true })
|
||||
return
|
||||
}
|
||||
|
||||
// This array is used to store all the roles for this reaction roles
|
||||
let roles = [args.reactions.create]
|
||||
|
||||
// Send the message that uses will use to get the role
|
||||
const roleMessage = await bot.helpers.sendMessage(interaction.channelId, {
|
||||
content: 'Pick your roles',
|
||||
components: getRoleButtons(roles),
|
||||
})
|
||||
|
||||
// Create a copy of the actionRow for the main message
|
||||
// NOTE: we use a copy so when we edit this actionRow the edits don't get applied to all the command executions, only this one, for example we do disable some buttons in some conditional cases
|
||||
const messageActionRow = structuredClone(messageActionRowTemplate)
|
||||
|
||||
await interaction.defer(true)
|
||||
const message = await interaction.respond(
|
||||
{
|
||||
content: 'Use the buttons in this message to edit the message below.',
|
||||
components: [messageActionRow],
|
||||
},
|
||||
{ isPrivate: true },
|
||||
)
|
||||
|
||||
if (!message) {
|
||||
await interaction.respond('❌ Unable to send the message correctly. Cancelling', { isPrivate: true })
|
||||
return
|
||||
}
|
||||
|
||||
// Create the collector for the menu
|
||||
const itemCollector = new ItemCollector<Interaction>()
|
||||
collectors.add(itemCollector)
|
||||
|
||||
// For the new reaction role, we need to keep track of what the user gave us
|
||||
let partialRoleInfo: Partial<(typeof roles)[number]> | undefined
|
||||
|
||||
itemCollector.onItem(async (i) => {
|
||||
// We need to verify the interaction is for us.
|
||||
if (i.message?.id !== message.id) {
|
||||
return
|
||||
}
|
||||
|
||||
// Save button
|
||||
if (i.data?.customId === 'reactionRoles-save') {
|
||||
// Remove this item collector from the list of collectors (we aren't correcting anymore)
|
||||
collectors.delete(itemCollector)
|
||||
|
||||
// Delete the edit message
|
||||
await i.deferEdit()
|
||||
await i.delete()
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// New button
|
||||
if (i.data?.customId === 'reactionRoles-add') {
|
||||
partialRoleInfo = {}
|
||||
|
||||
// Ask the user for the role
|
||||
await i.edit({ content: 'Pick a role for the new reaction role', components: [selectRoleActionRow] })
|
||||
return
|
||||
}
|
||||
|
||||
// New button - role select menu
|
||||
if (partialRoleInfo && i.data?.customId === 'reactionRoles-add-role') {
|
||||
const roleToAdd = i.data?.resolved?.roles?.first()
|
||||
|
||||
// Verify that we could get the role from discord
|
||||
if (!roleToAdd) {
|
||||
throw new Error('Unable to get the information for the role to add')
|
||||
}
|
||||
|
||||
// Save it to our partial role information
|
||||
partialRoleInfo.role = roleToAdd
|
||||
|
||||
// Ask the user for the color of the button
|
||||
await i.edit({
|
||||
content: 'Pick a color for the reaction role',
|
||||
components: [selectColorActionRow],
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// New button - color select menu
|
||||
if (partialRoleInfo && i.data?.customId === 'reactionRoles-add-color') {
|
||||
const color = parseInt(i.data?.values?.[0] ?? 'NaN')
|
||||
|
||||
// Verify that we could get the color information
|
||||
if (isNaN(color)) {
|
||||
throw new Error('Unable to get the information for the role to add')
|
||||
}
|
||||
|
||||
// Save the color to our partial
|
||||
partialRoleInfo.color = color
|
||||
|
||||
// Ask the user to input the emoji and optionally a label for the button
|
||||
await i.respond({
|
||||
title: 'Pick an emoji and label for the reaction role',
|
||||
components: [selectEmojiActionRow, selectLabelActionRow],
|
||||
customId: 'reactionRoles-add-modal',
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// New button - emoji & label modal
|
||||
if (partialRoleInfo && i.data?.customId === 'reactionRoles-add-modal') {
|
||||
// Ensure that we can get the channelId from the interaction
|
||||
if (!interaction.channelId) {
|
||||
throw new Error('Unable to get current channel')
|
||||
}
|
||||
|
||||
// Get the data from discord
|
||||
const emoji = i.data.components?.[0]?.components?.[0].value
|
||||
const label = i.data.components?.[1]?.components?.[0].value
|
||||
|
||||
// Verify that the emoji was given
|
||||
if (!emoji) {
|
||||
throw new Error('Unable to get the information for the role to add')
|
||||
}
|
||||
|
||||
// Save them to our partial
|
||||
partialRoleInfo.emoji = emoji
|
||||
partialRoleInfo.label = label
|
||||
|
||||
// Save role and display the new message editing the old one
|
||||
|
||||
// We are sure that in this place the entire object has been assembled
|
||||
roles.push(partialRoleInfo as (typeof roles)[number])
|
||||
|
||||
await bot.helpers.editMessage(interaction.channelId, roleMessage.id, {
|
||||
components: getRoleButtons(roles),
|
||||
})
|
||||
|
||||
// Clear our partial roleInfo, we are done with it
|
||||
partialRoleInfo = undefined
|
||||
// In case the delete button was disabled (all the roles were deleted) re-enable it
|
||||
messageActionRow.components[1]!.disabled = false
|
||||
|
||||
// Discord imposes a limit of 5 action rows and 5 buttons for actionRow = 25 buttons max
|
||||
// more than 25 will give an error, so we disable the new button
|
||||
if (roles.length === 25) {
|
||||
const button = messageActionRow.components[0] as ButtonComponent
|
||||
button.disabled = true
|
||||
}
|
||||
|
||||
// Show again the main edit menu
|
||||
await interaction.edit({
|
||||
content: 'Use the buttons in this message to edit the message below.',
|
||||
components: [messageActionRow],
|
||||
})
|
||||
|
||||
// Respond to the modal. A modal submit (type 5) interaction can't edit the original response
|
||||
await i.respond('Reaction role created successfully. You can use the message above to add/remove a role', { isPrivate: true })
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Remove button
|
||||
if (i.data?.customId === 'reactionRoles-remove') {
|
||||
// Clone the actionRow for the remove select menu, this is to prevent unwanted data to appear to other users
|
||||
const removeActionRow = structuredClone(removeActionRowTemplate)
|
||||
const selectMenu = removeActionRow.components[0] as SelectMenuComponent
|
||||
|
||||
// Add the possible values for this select menu
|
||||
for (const roleInfo of roles) {
|
||||
selectMenu.options.push({
|
||||
label: `${roleInfo.emoji} ${roleInfo.label ?? ''}`,
|
||||
value: roleInfo.role.id.toString(),
|
||||
})
|
||||
}
|
||||
|
||||
// Ask the user for what reaction role they want to remove
|
||||
await i.edit({
|
||||
content: 'Select what reaction role to remove',
|
||||
components: [removeActionRow],
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// Remove button - role select menu
|
||||
if (i.data?.customId === 'reactionRoles-remove-selectMenu') {
|
||||
// Ensure that we can get the channelId from the interaction
|
||||
if (!interaction.channelId) {
|
||||
throw new Error('Unable to get current channel')
|
||||
}
|
||||
|
||||
// Get the role to delete from discord
|
||||
const roleToRemove = i.data?.values?.[0]
|
||||
|
||||
// Ensure we got it
|
||||
if (!roleToRemove) {
|
||||
throw new Error('Unable to get the role to remove')
|
||||
}
|
||||
|
||||
await i.deferEdit()
|
||||
|
||||
// Remove the role from the list
|
||||
roles = roles.filter((roleInfo) => roleInfo.role.id.toString() !== roleToRemove)
|
||||
|
||||
// Edit the main button
|
||||
await bot.helpers.editMessage(interaction.channelId, roleMessage.id, {
|
||||
components: getRoleButtons(roles),
|
||||
})
|
||||
|
||||
// If the new button was disabled (we were at 25 buttons) we re-enable it
|
||||
const button = messageActionRow.components[0] as ButtonComponent
|
||||
button.disabled = false
|
||||
|
||||
// If we are at 0 roles, and the user tried to delete a role they will get locked in the menu, so we disable it
|
||||
if (roles.length === 0) {
|
||||
messageActionRow.components[1]!.disabled = true
|
||||
}
|
||||
|
||||
// Show the main edit ui (new, remove, save)
|
||||
await i.edit({
|
||||
content: 'Use the buttons in this message to edit the message below.',
|
||||
components: [messageActionRow],
|
||||
})
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// We don't know what code to run for this interaction
|
||||
throw new Error('Unknown button')
|
||||
})
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
export default command
|
||||
|
||||
// Interface to type the arguments that we receive from discord
|
||||
interface CommandArgs {
|
||||
reactions?: {
|
||||
create?: {
|
||||
role: Role
|
||||
emoji: string
|
||||
color: ButtonStyles
|
||||
label?: string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Templates/ActionRows for the command to then be referenced in the various part of the code
|
||||
|
||||
const messageActionRowTemplate: ActionRow = {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.Button,
|
||||
style: ButtonStyles.Success,
|
||||
customId: 'reactionRoles-add',
|
||||
emoji: {
|
||||
name: '➕',
|
||||
},
|
||||
label: 'Add',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
type: MessageComponentTypes.Button,
|
||||
style: ButtonStyles.Danger,
|
||||
customId: 'reactionRoles-remove',
|
||||
emoji: {
|
||||
name: '➖',
|
||||
},
|
||||
label: 'Remove',
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
type: MessageComponentTypes.Button,
|
||||
style: ButtonStyles.Success,
|
||||
customId: 'reactionRoles-save',
|
||||
emoji: {
|
||||
name: '✅',
|
||||
},
|
||||
label: 'Save',
|
||||
},
|
||||
],
|
||||
} as const
|
||||
|
||||
const removeActionRowTemplate: ActionRow = {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.SelectMenu,
|
||||
customId: 'reactionRoles-remove-selectMenu',
|
||||
maxValues: 1,
|
||||
minValues: 1,
|
||||
placeholder: 'Select roles',
|
||||
options: [],
|
||||
},
|
||||
],
|
||||
} as const
|
||||
|
||||
const selectRoleActionRow: ActionRow = {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.SelectMenuRoles,
|
||||
customId: 'reactionRoles-add-role',
|
||||
maxValues: 1,
|
||||
minValues: 1,
|
||||
placeholder: 'Select a role',
|
||||
},
|
||||
],
|
||||
} as const
|
||||
|
||||
const selectColorActionRow: ActionRow = {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.SelectMenu,
|
||||
customId: 'reactionRoles-add-color',
|
||||
options: [
|
||||
{ label: 'Blue', value: ButtonStyles.Primary.toString() },
|
||||
{ label: 'Green', value: ButtonStyles.Success.toString() },
|
||||
{ label: 'Grey', value: ButtonStyles.Secondary.toString() },
|
||||
{ label: 'Red', value: ButtonStyles.Danger.toString() },
|
||||
],
|
||||
},
|
||||
],
|
||||
} as const
|
||||
|
||||
const selectEmojiActionRow: ActionRow = {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.InputText,
|
||||
style: TextStyles.Short,
|
||||
customId: 'reactionRoles-add-emoji',
|
||||
label: 'Emoji for the reaction role',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
} as const
|
||||
|
||||
const selectLabelActionRow: ActionRow = {
|
||||
type: MessageComponentTypes.ActionRow,
|
||||
components: [
|
||||
{
|
||||
type: MessageComponentTypes.InputText,
|
||||
style: TextStyles.Short,
|
||||
customId: 'reactionRoles-add-label',
|
||||
label: 'Label for the reaction role [OPTIONAL]',
|
||||
// Discord imposed limit for button labels
|
||||
maxLength: 80,
|
||||
},
|
||||
],
|
||||
} as const
|
||||
|
||||
// Function to get all the actionRows with buttons for the reaction roles message
|
||||
function getRoleButtons(
|
||||
roles: Array<{
|
||||
role: Role
|
||||
emoji: string
|
||||
color: ButtonStyles
|
||||
label?: string | undefined
|
||||
}>,
|
||||
): ActionRow[] {
|
||||
const actionRows: ActionRow[] = []
|
||||
|
||||
// If there aren't any roles, we don't need any buttons
|
||||
if (roles.length === 0) return actionRows
|
||||
|
||||
// We add the components later, so we need to make typescript know that we are sure that it will be a compatibile components array
|
||||
actionRows.push({ type: MessageComponentTypes.ActionRow, components: [] as unknown as ActionRow['components'] })
|
||||
|
||||
for (const roleInfo of roles) {
|
||||
let actionRow = actionRows.at(-1)
|
||||
|
||||
// Ensure that we were able to get the actionRow
|
||||
if (!actionRow) {
|
||||
throw new Error('Unable to get actionRow')
|
||||
}
|
||||
|
||||
// If the actionRow is full (has 5 buttons) add a new one
|
||||
if (actionRow.components.length === 5) {
|
||||
actionRow = { type: MessageComponentTypes.ActionRow, components: [] as unknown as ActionRow['components'] }
|
||||
actionRows.push(actionRow)
|
||||
}
|
||||
|
||||
// Add the new button to this actionRow
|
||||
actionRow?.components.push({
|
||||
type: MessageComponentTypes.Button,
|
||||
style: roleInfo.color,
|
||||
emoji: {
|
||||
name: roleInfo.emoji,
|
||||
},
|
||||
label: roleInfo.label,
|
||||
customId: `reactionRoles-role-${roleInfo.role.id}`,
|
||||
})
|
||||
}
|
||||
|
||||
return actionRows
|
||||
}
|
||||
10
examples/reaction-roles/src/events/index.ts
Normal file
10
examples/reaction-roles/src/events/index.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
import type { EventHandlers } from '@discordeno/bot'
|
||||
import { event as interactionCreateEvent } from './interactionCreate.js'
|
||||
import { event as readyEvent } from './ready.js'
|
||||
|
||||
export const events = {
|
||||
interactionCreate: interactionCreateEvent,
|
||||
ready: readyEvent,
|
||||
} as Partial<EventHandlers>
|
||||
|
||||
export default events
|
||||
58
examples/reaction-roles/src/events/interactionCreate.ts
Normal file
58
examples/reaction-roles/src/events/interactionCreate.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
import { InteractionTypes, MessageComponentTypes, commandOptionsParser, type EventHandlers, type Interaction } from '@discordeno/bot'
|
||||
import type ItemCollector from '../collector.js'
|
||||
import commands from '../commands/index.js'
|
||||
|
||||
export const collectors = new Set<ItemCollector<Interaction>>()
|
||||
|
||||
export const event: EventHandlers['interactionCreate'] = async (interaction) => {
|
||||
// Give to all the collectors the interaction to use
|
||||
for (const collector of collectors) {
|
||||
collector.collect(interaction)
|
||||
}
|
||||
|
||||
// If the interaction is a command check if it is a command and run it
|
||||
if (interaction.type === InteractionTypes.ApplicationCommand) {
|
||||
if (!interaction.data) return
|
||||
|
||||
const command = commands.get(interaction.data.name)
|
||||
if (!command) return
|
||||
|
||||
try {
|
||||
await command.execute(interaction, commandOptionsParser(interaction))
|
||||
} catch (error) {
|
||||
console.error(error)
|
||||
}
|
||||
}
|
||||
|
||||
// If the interaction is a button it might be the button press on our reaction role message
|
||||
if (interaction.type === InteractionTypes.MessageComponent && interaction.data?.componentType === MessageComponentTypes.Button) {
|
||||
// The interaction is not a button press on the role button
|
||||
if (!interaction.data?.customId?.startsWith('reactionRoles-role-')) return
|
||||
if (!interaction.guildId || !interaction.member) return
|
||||
|
||||
// Remove the prefix and get the roleId
|
||||
const roleId = BigInt(interaction.data.customId.slice('reactionRoles-role-'.length))
|
||||
|
||||
// Check if we need to remove or add the role to the user
|
||||
const alreadyHasRole = !!interaction.member.roles.find((role) => role === roleId)
|
||||
|
||||
try {
|
||||
if (alreadyHasRole) {
|
||||
await interaction.bot.helpers.removeRole(interaction.guildId, interaction.user.id, roleId, `Reaction role button for role id ${roleId}`)
|
||||
await interaction.respond(`I removed from you the <@&${roleId}> role.`, { isPrivate: true })
|
||||
return
|
||||
}
|
||||
|
||||
// You will get an invalid request made if the bot attempts to give a bot role, a role higher then him hightest role, a link role or if it does not have the Manage Roles permission
|
||||
// This could be prevented by checking for the roles that the bot owns and the role that the bot is trying to add
|
||||
await interaction.bot.helpers.addRole(interaction.guildId, interaction.user.id, roleId, `Reaction role button for role id ${roleId}`)
|
||||
await interaction.respond(`I added to you the <@&${roleId}> role.`, { isPrivate: true })
|
||||
} catch {
|
||||
// Respond with an error message
|
||||
await interaction.respond(
|
||||
'I could not give you the role. Possible reasons are:\n- My permissions are not configured correctly, make sure i have the `Manage Roles` permission\n- The role is **above** my hightest role in the server setup\n- The role does not exist or is non-manageable (for example: bot roles, link roles or @everyone)',
|
||||
{ isPrivate: true },
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
7
examples/reaction-roles/src/events/ready.ts
Normal file
7
examples/reaction-roles/src/events/ready.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
import type { EventHandlers } from '@discordeno/bot'
|
||||
import { bot } from '../index.js'
|
||||
|
||||
export const event: EventHandlers['ready'] = () => {
|
||||
// Print to the console when the bot has connected to discord and is ready to handle the events
|
||||
bot.logger.info('The bot is ready!')
|
||||
}
|
||||
39
examples/reaction-roles/src/index.ts
Normal file
39
examples/reaction-roles/src/index.ts
Normal file
@@ -0,0 +1,39 @@
|
||||
import { createBot } from '@discordeno/bot'
|
||||
import { config } from 'dotenv'
|
||||
|
||||
import events from './events/index.js'
|
||||
|
||||
config()
|
||||
const token = process.env.TOKEN
|
||||
|
||||
// Ensure the existence of the TOKEN env
|
||||
if (!token) throw new Error('The TOKEN environment variable needs to be defined.')
|
||||
|
||||
export const bot = createBot({
|
||||
token,
|
||||
events,
|
||||
})
|
||||
|
||||
// Setup for the desiredProperties
|
||||
|
||||
bot.transformers.desiredProperties.user.id = true
|
||||
|
||||
bot.transformers.desiredProperties.message.id = true
|
||||
|
||||
bot.transformers.desiredProperties.member.roles = true
|
||||
|
||||
bot.transformers.desiredProperties.interaction.id = true
|
||||
bot.transformers.desiredProperties.interaction.data = true
|
||||
bot.transformers.desiredProperties.interaction.type = true
|
||||
bot.transformers.desiredProperties.interaction.user = true
|
||||
bot.transformers.desiredProperties.interaction.token = true
|
||||
bot.transformers.desiredProperties.interaction.member = true
|
||||
bot.transformers.desiredProperties.interaction.message = true
|
||||
bot.transformers.desiredProperties.interaction.guildId = true
|
||||
bot.transformers.desiredProperties.interaction.channelId = true
|
||||
|
||||
bot.transformers.desiredProperties.role.id = true
|
||||
|
||||
await bot.start()
|
||||
|
||||
process.on('unhandledRejection', bot.logger.error)
|
||||
8
examples/reaction-roles/src/register-commands.ts
Normal file
8
examples/reaction-roles/src/register-commands.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
import commands from './commands/index.js'
|
||||
import { bot } from './index.js'
|
||||
|
||||
const guildId = 'REPLACE WITH YOUR GUILD ID'
|
||||
|
||||
await bot.rest
|
||||
.upsertGuildApplicationCommands(guildId, [...commands.values()])
|
||||
.catch((e) => bot.logger.error('There was an error when updating the global commands', e))
|
||||
14
examples/reaction-roles/tsconfig.json
Normal file
14
examples/reaction-roles/tsconfig.json
Normal 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
|
||||
}
|
||||
}
|
||||
2101
examples/reaction-roles/yarn.lock
Normal file
2101
examples/reaction-roles/yarn.lock
Normal file
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -219,6 +219,24 @@ const config: Config = {
|
||||
prism: {
|
||||
theme: themes.github,
|
||||
darkTheme: themes.dracula,
|
||||
magicComments: [
|
||||
// Docusaurus default magic comment
|
||||
{
|
||||
className: 'theme-code-block-highlighted-line',
|
||||
line: 'highlight-next-line',
|
||||
block: { start: 'highlight-start', end: 'highlight-end' },
|
||||
},
|
||||
{
|
||||
className: 'theme-code-block-add',
|
||||
line: 'insert-next-line',
|
||||
block: { start: 'insert-start', end: 'insert-end' },
|
||||
},
|
||||
{
|
||||
className: 'theme-code-block-remove',
|
||||
line: 'remove-next-line',
|
||||
block: { start: 'remove-start', end: 'remove-end' },
|
||||
},
|
||||
],
|
||||
additionalLanguages: ['bash'],
|
||||
},
|
||||
} satisfies ThemeConfig,
|
||||
|
||||
@@ -36,3 +36,43 @@
|
||||
--ifm-footer-link-color: #cfcfcf;
|
||||
--ifm-footer-background-color: #1c1c1c;
|
||||
}
|
||||
|
||||
.theme-code-block-add {
|
||||
--border-left: 10px;
|
||||
|
||||
position: relative;
|
||||
background-color: #08ff0020;
|
||||
display: block;
|
||||
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||
padding: 0 calc(var(--ifm-pre-padding));
|
||||
}
|
||||
|
||||
.theme-code-block-add::before {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
|
||||
content: '+';
|
||||
|
||||
color: #00da86ce;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.theme-code-block-remove {
|
||||
--border-left: 10px;
|
||||
|
||||
position: relative;
|
||||
background-color: #ff000020;
|
||||
display: block;
|
||||
margin: 0 calc(-1 * var(--ifm-pre-padding));
|
||||
padding: 0 calc(var(--ifm-pre-padding));
|
||||
}
|
||||
|
||||
.theme-code-block-remove::before {
|
||||
position: absolute;
|
||||
left: 4px;
|
||||
|
||||
content: '-';
|
||||
|
||||
color: #da001dce;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user