Загрузка...

[ADVANCED] Создание Discord ботов используя Sapphire.js

Тема в разделе Статьи создана пользователем awalki 24 сен 2024. (поднята 3 июл 2025) 772 просмотра

  1. awalki
    awalki Автор темы 24 сен 2024 продам 319 29 июн 2021
    Доброго времени суток уважаемые форумчане

    Листал я значит список статей и наталкиваюсь на статью от dophaminov - ТЫК и понял, что оказывается по дискорд ботам очень мало полезной информации.

    В этой статье хотелось бы более подробно затронуть тему создания дискорд ботов. Мы рассмотрим создание дискорд ботов с помощью фреймворка Sapphire.js (надстройка над discord.js)

    Рекомендую к прочтению тем людям, которые хотят создавать больших и масштабируемых ботов, а не шалости с использованием детских фреймворков.

    В данной статье я затрону следующие темы:
    1. Про Sapphire.js
    2. Структура бота
    3. Создание команд
    4. Создание слушателей (listeners)
    5. Создание кнопок, выборочных меню, модалей и управление ими
    6. Интеграция с MongoDB

    Sapphire - это объектно-ориентированный фреймворк нового поколения для ботов Discord.js.

    Sapphire - это фреймворк, созданный сообществом, цель которого - предоставить вам все возможности, необходимые для создания вашего Discord-бота.

    История многих других фреймворков для ботов Discord (как для Node.js, так и для
    других языков), вдохновлявших Sapphire, он стал самым современным
    опыт написания кода.

    Возможности фреймворка
    Расширенная поддержка плагинов
    Поддерживает CommonJS и ESM
    Полностью модульный и расширяемый
    Разработан с учетом первоклассной поддержки TypeScript
    Включает в себя дополнительные утилиты, которые можно использовать в любом проекте
    Настройка рабочей среды для разработки ботов

    Скачиваем Node.js - Node.js (LTS)

    Ставим зависимости
    Код
    npm i -g yarn
    yarn add typescript
    yarn add ts-node
    yarn add @sapphire/framework discord.js



    Инициализируем проект
    Код
    yarn init
    [IMG]
    Мы будем использовать данную структуру, поскольку она является оптимальной и расширяемой
    [IMG]

    Создаем tsconfig.json
    Код
    {
    "compilerOptions": {
    "module": "CommonJS",
    "target": "ESNext",
    "outDir": "dist",
    "lib": [
    "ESNext",
    "ESNext.Array",
    "ESNext.AsyncIterable",
    "ESNext.Intl",
    "ESNext.Symbol",
    "DOM"
    ],
    "sourceMap": true,
    "inlineSources": true,
    "incremental": true,
    "esModuleInterop": true,
    "experimentalDecorators": true,
    "emitDecoratorMetadata": true,
    "skipLibCheck": true
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules"]
    }

    Также необходимо немного изменить наш package.json
    Код
    {
    "name": "mydiscordbot",
    "scripts": {
    "start": "ts-node src/index.ts"
    },
    "version": "1.0.0",
    "license": "MIT",
    "main": "src/index.ts",
    "dependencies": {
    "@sapphire/framework": "^5.2.1",
    "discord.js": "^14.16.2",
    "dotenv": "^16.4.5",
    "mongoose": "^8.6.3",
    "ts-node": "^10.9.2",
    "typescript": "^5.6.2"
    }
    }

    Мы добавили скрипт, который позволит автоматически перезагружать наш typescript без ручной компиляции :finger_up:
    Наша базовая структура готова, далее уже можно создавать файлы и писать код, но сначала необходимо создать самого бота, это мы рассмотрим в следующем этапе
    Cоздание вашего Discord бота
    1. Зайдите на портал разработчиков дискорда
    2. Войдите в ваш Discord аккаунт
    3. Выберите классное имя для вашего бота
    4. Примите Discord ToS
    5. Нажмите кнопку "Создать"
    [IMG]

    Тыкаете на кнопку "Bot"
    [IMG]

    Включаете все разрешения для бота
    [IMG]
    спиздил картинки
    Приглашаем бота
    [IMG]

    Убедитесь, что выбрали эти разрешения:
    -
    ⁡bot

    -
    ⁡application.commands


    Приглашаем бота на сервер
    [IMG]

    Возвращайтесь во вкладку "Bot" и нажимаете кнопку "Reset Token". Мы будем использовать этот токен для того, чтобы управлять нашим ботом. Распространять токен ни в коем случае нельзя!
    [IMG]
    Помните мы ранее создавали .env файлик в корне проекта?

    Так вот сейчас он нам пригодится, его нужно заполнить следующим образом:
    [IMG]
    Не забудьте добавить .env файлик в .gitignore -> .env, а также .gitignore -> node_modules

    Создаем файл по пути src/client/client.ts и заполняем его следующим образом:
    JS
    import {
    SapphireClient,
    ApplicationCommandRegistries,
    RegisterBehavior,
    } from "@sapphire/framework";
    import { GatewayIntentBits } from "discord.js";

    export default class Client extends SapphireClient {
    public constructor() {
    super({
    intents: [
    GatewayIntentBits.Guilds,
    GatewayIntentBits.GuildMessages,
    GatewayIntentBits.GuildMembers,
    GatewayIntentBits.GuildPresences,
    GatewayIntentBits.MessageContent,
    ],
    });
    }
    public override login(token?: string) {
    ApplicationCommandRegistries.setDefaultBehaviorWhenNotIdentical(
    RegisterBehavior.BulkOverwrite,
    );
    return super.login(token); }

    }

    Открываем index.ts и вызываем метод авторизации с указанием нашего токена из переменной окружения (в нашем случае .env файлик)
    JS
    import "dotenv/config"

    import Client from "./client/client";
    new Client().login(process.env.TOKEN);

    Первый запуск бота
    Введите в терминал команду `yarn start`
    Если вы все правильно сделали, то бот должен запустится, но он пока-что ничего не делает. Давайте добавим нашу первую slash-команду
    [IMG]
    Перейдем в ранее созданый пакет commands и создадим файл ping.ts

    В который мы вставим следующий код:
    JS
    import { isMessageInstance } from '@sapphire/discord.js-utilities';
    import { Command } from '@sapphire/framework';

    export class PingCommand extends Command {
    public constructor(context: Command.LoaderContext, options: Command.Options) {
    super(context, { ...options });
    }

    public override registerApplicationCommands(registry: Command.Registry) {
    registry.registerChatInputCommand((builder) =>
    builder.setName('ping').setDescription('Ping bot to see if it is alive')
    );
    }

    public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
    const msg = await interaction.reply({ content: `Ping?`, ephemeral: true, fetchReply: true });

    if (isMessageInstance(msg)) {
    const diff = msg.createdTimestamp - interaction.createdTimestamp;
    const ping = Math.round(this.container.client.ws.ping);
    return interaction.editReply(`Pong ! (Round trip took: ${diff}ms. Heartbeat: ${ping}ms.)`);
    }

    return interaction.editReply('Failed to retrieve ping :(');
    }
    }


    Запускаем бота при помощи команды `yarn start`. У нас в консоли должно было показать, что зарегистрирована 1 глобальная команда
    [IMG]

    Для наглядности попробуем активировать команду на сервере.
    [IMG]
    Важное замечание:
    В пакете commands вы можете группировать команды по папкам. Sapphire же сделает за нас всю работу и зарегистрирует эти команды. :finger_up:
    Тут мы рассмотрим как работать с кнопками, модальными окнами и выпадающими меню. Фреймворк Sapphire очень упрощает работу с взаимодействиями и делает половину работы за нас
    В пакете interaction-handlers создадим `helloworld-btn.ts`, в котором напишем следующий код:
    JS
    import { InteractionHandler, InteractionHandlerTypes } from '@sapphire/framework';
    import type { ButtonInteraction } from 'discord.js';

    export class ButtonHandler extends InteractionHandler {
    public constructor(ctx: InteractionHandler.LoaderContext, options: InteractionHandler.Options) {
    super(ctx, {
    ...options,
    interactionHandlerType: InteractionHandlerTypes.Button
    });
    }

    public override parse(interaction: ButtonInteraction) {
    if (interaction.customId !== 'my-awesome-button') return this.none();

    return this.some();
    }

    public async run(interaction: ButtonInteraction) {
    await interaction.reply({
    content: 'Hello from a button interaction handler!',
    // Let's make it so only the person who pressed the button can see this message!
    ephemeral: true
    });
    }
    }


    Вместо `my-awesome-button` поставьте будущий айди вашей кнопки, определим мы его чутка позже. В моем случае я поставлю `helloworld-btn`

    Как теперь использовать это чудо?
    В ранее созданном ping.ts добавляем данные импорты:
    ⁡import {ActionRowBuilder, ButtonBuilder, ButtonStyle} from "discord.js";


    Далее немного видоизменяем ping.ts
    JS
    public override async chatInputRun(interaction: Command.ChatInputCommandInteraction) {
    const msg = await interaction.reply({ content: `Ping?`, ephemeral: true, fetchReply: true });
    const row = new ActionRowBuilder<ButtonBuilder>()
    .addComponents(
    new ButtonBuilder()
    .setLabel("Hello world!")
    .setCustomId("helloworld-btn")
    .setStyle(ButtonStyle.Success)
    .setEmoji("")
    )

    if (isMessageInstance(msg)) {
    const diff = msg.createdTimestamp - interaction.createdTimestamp;
    const ping = Math.round(this.container.client.ws.ping);
    return interaction.editReply({
    content: `Pong ! (Round trip took: ${diff}ms. Heartbeat: ${ping}ms.)`,
    components: [row]
    });
    }

    return interaction.editReply('Failed to retrieve ping :(');
    }

    Результат:
    [IMG]
    С модалами дела обстоят похоже, создается сначала само окно с вопросами, потом оно вызывается извне, например по нажатию той же кнопки.
    Покажу вам пример из моего реального дискорд бота:

    Окно с вопросами:

    JS
    import {
    ActionRowBuilder,
    ModalBuilder,
    TextInputBuilder,
    TextInputStyle,
    } from "discord.js";

    export default class ApplicationModal {
    public async sendApplicationModal(interaction) {
    // НАЗВАНИЕ МОДАЛЬНОГО ОКНА
    const modal = new ModalBuilder()
    .setCustomId(`modal-open`)
    .setTitle("Модальное окно");

    modal.setComponents(
    new ActionRowBuilder<TextInputBuilder>().addComponents(
    // ВОПРОС 1
    new TextInputBuilder()
    .setCustomId("nickInput")
    .setLabel("Ваш ник в Minecraft")
    .setRequired(true)
    .setStyle(TextInputStyle.Short),
    ),
    new ActionRowBuilder<TextInputBuilder>().addComponents(
    new TextInputBuilder()
    .setCustomId("ageInput")
    .setLabel("Ваш возраст")
    .setRequired(true)
    .setStyle(TextInputStyle.Short),
    ),
    new ActionRowBuilder<TextInputBuilder>().addComponents(
    new TextInputBuilder()
    .setCustomId("loveInput")
    .setLabel("Чем вас затягивает Minecraft")
    .setRequired(true)
    .setStyle(TextInputStyle.Paragraph),
    ),
    new ActionRowBuilder<TextInputBuilder>().addComponents(
    new TextInputBuilder()
    .setCustomId("xpInput")
    .setLabel("Как давно играете в Minecraft")
    .setRequired(true)
    .setStyle(TextInputStyle.Short),
    ),
    new ActionRowBuilder<TextInputBuilder>().addComponents(
    new TextInputBuilder()
    .setCustomId("whereInput")
    .setLabel("Откуда узнали про нас")
    .setRequired(true)
    .setStyle(TextInputStyle.Short),
    ),
    );
    return await interaction.showModal(modal);
    }
    }

    Логика работы окна:

    [CODE=js]import {
    InteractionHandler,
    InteractionHandlerTypes,
    } from "@sapphire/framework";
    import {
    ButtonBuilder,
    ModalSubmitInteraction,
    ActionRowBuilder,
    ButtonStyle,
    EmbedBuilder, User, BaseClient,
    } from "discord.js";

    export class ModalHandler extends InteractionHandler {
    public constructor(
    ctx: InteractionHandler.LoaderContext,
    options: InteractionHandler.Options,
    ) {
    super(ctx, {
    ...options,
    interactionHandlerType: InteractionHandlerTypes.ModalSubmit,
    });
    }

    public override parse(interaction: ModalSubmitInteraction) {
    if (interaction.customId !== "modal-open") return this.none();

    return this.some();
    }

    public async run(interaction: ModalSubmitInteraction) {
    const user = await interaction.client.users.fetch("1252079083022581822");
    const embed: EmbedBuilder = new EmbedBuilder()
    .setTitle("Пришла новая заявка!")
    .setDescription(
    `
    Ник: ${interaction.fields.getTextInputValue("nickInput")}
    Возраст: ${interaction.fields.getTextInputValue("ageInput")}
    Чем затягивает: ${interaction.fields.getTextInputValue("loveInput")}
    Как давно играет: ${interaction.fields.getTextInputValue("xpInput")}
    Откуда узнал: ${interaction.fields.getTextInputValue("whereInput")}
    `,
    )
    .setFooter({
    text: interaction.user.id,
    });
    await user.send({
    embeds: [embed],
    components: [
    new ActionRowBuilder<ButtonBuilder>().addComponents(
    new ButtonBuilder()
    .setLabel("Принять")
    .setEmoji("")
    .setCustomId("accept-btn")
    .setStyle(ButtonStyle.Success),
    new ButtonBuilder()
    .setLabel("Отклонить")
    .setEmoji("")
    .setCustomId("decline-btn")
    .setStyle(ButtonStyle.Danger),
    ),
    ],
    });
    await interaction.reply({
    content: `Ваша заявка успешно отправлена на рассмотрение!`,
    ephemeral: true,
    });
    }
    }[/CODE]Вывод окна пользователю используя кнопку:

    [CODE=js]import {
    InteractionHandler,
    InteractionHandlerTypes,
    } from "@sapphire/framework";
    import {
    type StringSelectMenuInteraction,
    type ButtonInteraction,
    } from "discord.js";
    import ApplicationModal from "../../templates/modals/applicationModal";

    export class MessageComponentHandler extends InteractionHandler {
    public constructor(
    ctx: InteractionHandler.LoaderContext,
    options: InteractionHandler.Options,
    ) {
    super(ctx, {
    ...options,
    interactionHandlerType: InteractionHandlerTypes.MessageComponent,
    });
    }

    public override parse(
    interaction: ButtonInteraction | StringSelectMenuInteraction,
    ) {
    if (interaction.customId !== "send-app") return this.none();

    return this.some();
    }

    public async run(
    interaction: ButtonInteraction | StringSelectMenuInteraction,
    ) {
    if (interaction.isButton()) {
    await new ApplicationModal().sendApplicationModal(interaction);
    }
    }
    }[/CODE]
    Выпадающие меню, как способ хранения множества информации в маленьком месте

    Код обработчика выборочных меню

    [CODE=js]import {
    InteractionHandler,
    InteractionHandlerTypes,
    } from "@sapphire/framework";
    import type { StringSelectMenuInteraction } from "discord.js";

    export class MenuHandler extends InteractionHandler {
    public constructor(
    ctx: InteractionHandler.LoaderContext,
    options: InteractionHandler.Options,
    ) {
    super(ctx, {
    ...options,
    interactionHandlerType: InteractionHandlerTypes.SelectMenu,
    });
    }

    public override parse(interaction: StringSelectMenuInteraction) {
    if (interaction.customId !== "our-social") return this.none();

    return this.some();
    }

    public async run(interaction: StringSelectMenuInteraction) {
    await interaction.reply({
    // Remember how we can have multiple values? Let's get the first one!
    content: `${interaction.values[0]}`,
    ephemeral: true,
    });
    }
    }[/CODE]
    Реальный пример использования выборочных меню для отображения социальных сетей дискорд сервера
    [CODE=js]import { Command } from "@sapphire/framework";
    import { type ChatInputCommandInteraction } from "discord.js";
    import { ApplyOptions } from "@sapphire/decorators";
    import { ApplicationCommandRegistry } from "@sapphire/framework";
    import { StringSelectMenuOptionBuilder, EmbedBuilder } from "discord.js";
    import { StringSelectMenuBuilder } from "discord.js";
    import { ActionRowBuilder } from "discord.js";

    @ApplyOptions<Command.Options>({
    name: "our-social",
    description: "Возвращает соц-сети сервера",
    })
    export default class SocialCommand extends Command {
    public override async registerApplicationCommands(
    registry: ApplicationCommandRegistry,
    ) {
    registry.registerChatInputCommand((command) => {
    command.setName(this.name).setDescription(this.description);
    this.container.logger.info(`Command ${this.name} was successfully registered.`)
    });
    }
    public async chatInputRun(interaction: ChatInputCommandInteraction) {
    const embed = new EmbedBuilder()
    .setTitle(`Наши социальные сети`)
    .setDescription(
    `Воспользуйтесь выборочным меню ниже,\nчтобы выбрать интересующую вас социальную сеть.`,
    );

    await interaction.reply({
    embeds: [embed],
    ephemeral: true,
    components: [
    new ActionRowBuilder<StringSelectMenuBuilder>({
    components: [
    new StringSelectMenuBuilder()
    .setCustomId(`our-social`)
    .setMinValues(1)
    .setMaxValues(1)
    .setOptions(
    new StringSelectMenuOptionBuilder({
    label: `Youtube`,
    value: `https://www.youtube.com/хуй`,
    }),
    new StringSelectMenuOptionBuilder({
    label: `TikTok`,
    value: `https://tiktok.com/хуй`,
    }),
    new StringSelectMenuOptionBuilder({
    label: `Twitch`,
    value: `https://www.twitch.tv/хуй`,
    }),
    ),
    ],
    }),
    ],
    });
    }
    }
    [/CODE]
    Мы познакомились с богатыми возможностями взаимодействий через фреймворк Sapphire, тут были перечислены не все взаимодействия, подробнее вы можете посмотреть в официальной документации фреймворка: ТЫК
    Слушатели позволяют выполнять определенный кусочек кода при изменений условий на стороне вашего сервера, например:
    - Зашел новый участник (Написать ему приветственное сообщение в лс)
    - Бот присоединился к серверу (Отправить справку по использованию)

    Создание простого слушателя, при запуске бота. В пакете listeners создаете файл ready.ts и записываете следующее
    [CODE=js]import { Listener } from '@sapphire/framework';
    import type { Client } from 'discord.js';

    export class ReadyListener extends Listener {
    public run(client: Client) {
    const { username, id } = client.user!;
    this.container.logger.info(`Successfully logged in as ${username} (${id})`);
    }
    }[/CODE]
    Список всех слушателей можно посмотреть тут - ТЫК
    Бесплатная БД подойдет для небольшого бота. Для более серьезных нужно будет покупать тариф получше. Нам пока-что хватит и бесплатного :finger_up:

    Переходим на сайт MongoDB - тЫк и регистрируемся
    [IMG]
    Тыкаем на Build a Cluster и выставляем следующие настройки
    [IMG]

    У вас вылезет окошко с выбором метода подключения базы данных, выбираете Driver. Далее вам должна показаться ссылка с адресом на вашу базу данных. Эту ссылку мы пропишем в .env файл.
    [IMG]
    В пакете mongo создаем файл mongo.ts и прописываем следующий код, не забудьте установить
    ⁡yarn add mongoose

    [CODE=js]import mongoose from "mongoose";

    export default class MongoDB {
    public async connect(token?: string) {
    try {
    await mongoose.connect(token)
    console.log(`Successfully connected to MongoDB`)
    } catch (error) {
    console.error(error)
    }
    }
    }[/CODE]
    В index.ts нам нужно немного поменять код, инициализируем метод подключения к БД

    [CODE=js]import "dotenv/config"

    import MongoDB from "./mongo/mongo";
    new MongoDB().connect(process.env.DATABASE_TOKEN);
    import Client from "./client/client";
    new Client().login(process.env.TOKEN);[/CODE]
    Пробуем запустить бота и видим следующее:
    [IMG]
    Если вы все сделали правильно, вы получите сообщение об успешном подключении к MongoDB.
    В пакете mongo создаем еще один пакет schemas, а уже в нем файл user.ts
    Вводим следующий код:
    [CODE=js]import mongoose from "mongoose";


    const userSchema = new mongoose.Schema({
    userId: { type: String, required: true },
    })

    const UserSchema = mongoose.model('User', userSchema);

    export { UserSchema };
    [/CODE]
    По вашей надобности вы можете добавить больше полей, но в примере пока ограничимся одним.

    Как теперь это использовать? Например мы можем создавать пользователя, после того, как он зашел на сервер. Начнем с написания слушателя участников.
    [CODE=js]import { Listener } from '@sapphire/framework';
    import { Client } from 'discord.js';
    import { UserSchema } from "../../mongo/schemas/user";
    import mongoose from "mongoose";
    import SendPersonalMessage from '../functions/sendPM';
    import AddRole from '../functions/addRole';
    import WelcomeEmbed from '../templates/embeds/not_callable/welcome';

    export abstract class MemberListener extends Listener {
    public constructor(context: Listener.LoaderContext, options: Listener.Options) {
    super(context, {
    ...options,
    once: false,
    event: 'guildMemberAdd'
    });
    }
    public async run(client: Client) {
    const { username, id } = client.user!;
    const embed = await new WelcomeEmbed().embed()

    this.container.logger.info(`Присоединился новый участник ${username} (${id}).`);
    userProfile = await new UserSchema({
    _id: new mongoose.Types.ObjectId(),
    userId: id,
    });
    }
    }[/CODE]После того, как зайдет новый пользователь на сервер, он сразу же добавится в нашу базу данных.
    Спасибо за прочтение столь длинной статьи, надеюсь она была тебе полезна. Дальше советую полностью прочитать документацию discord.js и sapphire.js, чтобы лучше понять, как работают эти библиотеки и Discord API в целом.

    ps: не бейте палками если увидите не код, а какую-то хуйню. какие-то приколы редактора лзт
     
    Этот материал оказался полезным?
    Вы можете отблагодарить автора темы путем перевода средств на баланс
    Отблагодарить автора
    24 сен 2024 Изменено
    1. Toil
      awalki, пиздец у тебя там раъебался код
    2. awalki Автор темы
      Toil, да пиздец
  2. Yowori
    Yowori 24 сен 2024 Эльфографика грядёт ~ https://lolz.live/threads/7861550/ 15 708 3 июн 2019
    У тебя немношко код поплыл с сайзами, статья полезная лайкусик
     
    1. awalki Автор темы
      Yowori, походу какое-то форумное ограничение на создание блоков кода. убрал сайзы, но блок все равно не отрисовывается
      24 сен 2024 Изменено
Загрузка...
Top