RE:NODE

Приложения11 мин чтения

Бот для Discord на C#: Discord.Net, DSharpPlus или NetCord

Как написать и разместить бота для Discord на C#: выбор между Discord.Net, DSharpPlus и NetCord, gateway intents, slash-команды, Generic Host и работа 24/7.

0 прочтений

Бот для Discord на C# - это консольное .NET-приложение, которое держит одно исходящее WebSocket-соединение с gateway Discord и реагирует на события. Выберите библиотеку - Discord.Net самая распространённая и лучше всего задокументированная, DSharpPlus - давняя альтернатива, NetCord - самая новая и сделана под современный .NET, - включите только нужные gateway intents, регистрируйте slash-команды, а не разбирайте текст сообщений, и запускайте всё под .NET Generic Host, чтобы бот правильно запускался, писал логи и завершался. Ему не нужен входящий порт, нужно очень мало CPU и от 60 до 200 МБ памяти для большинства ботов, поэтому самого маленького тарифа хостинга обычно достаточно.

В этой статье бот на Discord.Net собирается от пустого проекта до slash-команды, а затем разобрано, что меняется, когда он круглосуточно работает на сервере.

Выбор библиотеки#

Все три библиотеки покрывают gateway, REST API, slash-команды, кнопки, меню выбора и модальные окна. Различаются они зрелостью, стилем и тем, насколько быстро каждая следует за новейшими возможностями Discord.

БиблиотекаПакетСтильПримечания
Discord.NetDiscord.NetНа событиях, зрелаяСамая большая база пользователей и больше всего примеров
DSharpPlusDSharpPlusНа событиях, пакеты расширенийВерсия 5 долго в prerelease; проверьте, какую используют руководства
NetCordNetCordСовременный .NET, хостинг на первом местеНовее, только для актуального .NET, близка к Discord API

Выбирайте Discord.Net, если хотите, чтобы ответ на любой вопрос находился одним поиском. Выбирайте NetCord, если начинаете с нуля на .NET 8 или новее и любите библиотеки, изначально спроектированные вокруг внедрения зависимостей и Generic Host. Выбирайте DSharpPlus, если уже её знаете; в противном случае проверьте текущее состояние её версии 5, прежде чем на неё полагаться, потому что примеры, написанные для версии 4, напрямую не переносятся.

Примеры ниже используют Discord.Net 3.x. Понятия - intents, регистрация команд, подтверждение взаимодействий, хост - принадлежат Discord, а не библиотеке, и применимы ко всем трём.

Приложение, токен и intents#

Прежде чем писать код, создайте бота в Discord Developer Portal:

  1. Создайте приложение через New Application, затем откройте страницу Bot.
  2. Сбросьте и скопируйте токен. Он показывается один раз. Любой, у кого он есть, может действовать от имени вашего бота на каждом сервере, где тот состоит.
  3. Включите привилегированные intents, которые нужны боту (см. ниже).
  4. В разделе OAuth2 сгенерируйте ссылку-приглашение со scope bot и applications.commands и только с теми правами, которые бот действительно использует. Откройте её и добавьте бота на тестовый сервер.

Intents определяют, какие события вам присылает Discord. Большинство из них обычные; три привилегированные и должны быть дополнительно включены в портале:

IntentПривилегированныйНужен для
GuildsНетПочти всего - данных о серверах и каналах
GuildMessagesНетСобытий сообщений на серверах
MessageContentДаТекста сообщений, адресованных не боту
GuildMembersДаСобытий входа и выхода участников, полных списков участников
GuildPresencesДаСтатуса в сети и активностей

На MessageContent попадаются все. Без него события сообщений всё равно приходят, но Content - пустая строка, кроме сообщений, где упомянут бот, или личных сообщений ему. Боту на slash-командах этот intent вообще не нужен, и это главная причина строить бота на slash-командах. Когда бот состоит на 100 и более серверах, привилегированные intents требуют одобрения Discord через верификацию бота, так что не запрашивайте то, чем не пользуетесь.

Та же сдержанность относится к правам в ссылке-приглашении. Есть соблазн поставить галочку Administrator, чтобы ничего никогда не падало с «Missing Permissions», и это превращает утёкший токен из досадной неприятности в потерю каждого сервера, где состоит бот: атакующий с ботом-администратором может удалять каналы, банить участников и раздавать роли. Выдавайте конкретные права, которые используют команды, - Send Messages, Embed Links, Manage Roles, если бот назначает роли, - и помните, что иерархия ролей всё равно действует: бот может управлять только ролями ниже своей высшей роли, какие бы права у него ни были. Тот же довод для стороны хостинга приводится в статье субпользователи и минимальные привилегии.

Минимальный бот на Generic Host#

Создайте проект worker и добавьте пакеты:

bash
$ dotnet new worker -n TavernBot$ cd TavernBot$ dotnet add package Discord.Net

Generic Host бесплатно даёт конфигурацию, логирование, внедрение зависимостей и чистое завершение. Сам бот - hosted service, который запускает клиент, когда стартует хост, и останавливает его, когда хост останавливается:

Program.cs
using Discord;using Discord.Interactions;using Discord.WebSocket;var builder = Host.CreateApplicationBuilder(args);builder.Services.AddSingleton(new DiscordSocketConfig{    GatewayIntents = GatewayIntents.Guilds});builder.Services.AddSingleton<DiscordSocketClient>();builder.Services.AddSingleton(sp =>    new InteractionService(sp.GetRequiredService<DiscordSocketClient>()));builder.Services.AddHostedService<BotService>();builder.Build().Run();
BotService.cs
public sealed class BotService(    DiscordSocketClient client,    InteractionService interactions,    IServiceProvider services,    IConfiguration config,    ILogger<BotService> log) : IHostedService{    public async Task StartAsync(CancellationToken ct)    {        client.Log += m => { log.LogInformation("{Message}", m.ToString()); return Task.CompletedTask; };        await interactions.AddModulesAsync(typeof(BotService).Assembly, services);        client.Ready += async () =>            await interactions.RegisterCommandsToGuildAsync(config.GetValue<ulong>("Discord:GuildId"));        client.InteractionCreated += async i =>            await interactions.ExecuteCommandAsync(new SocketInteractionContext(client, i), services);        await client.LoginAsync(TokenType.Bot, config["Discord:Token"]);        await client.StartAsync();    }    public async Task StopAsync(CancellationToken ct)    {        await client.StopAsync();        await client.LogoutAsync();    }}

Многие туториалы обходятся без хоста и заканчивают Main строкой await Task.Delay(-1), чтобы процесс жил. Это работает, пока вам не понадобится что-то ещё: чистого завершения нет, так что бота убивают посреди запроса при каждом перезапуске; внедрения зависимостей нет, так что каждый модуль создаёт собственное подключение к базе данных; системы конфигурации нет, так что токен в итоге прописывается прямо в коде. Хост стоит дюжины лишних строк и убирает все три проблемы. Это также означает, что тот же бот позже можно без изменений перенести в приложение ASP.NET Core, поскольку веб-приложение - тоже Generic Host.

Токен берётся из конфигурации, так что на сервере это переменная окружения Discord__Token, а не строка в коде. Двойное подчёркивание объясняется в статье конфигурация и секреты в ASP.NET Core; в worker работает та же система конфигурации. Если токен когда-нибудь попадёт в публичный репозиторий, немедленно сбросьте его в портале. Discord участвует в сканировании секретов GitHub и часто сам аннулирует утёкшие токены, но это спасение, а не план.

Slash-команды#

С фреймворком взаимодействий команда - это метод модуля:

Modules/GeneralModule.cs
public sealed class GeneralModule : InteractionModuleBase<SocketInteractionContext>{    [SlashCommand("ping", "Check the bot is alive")]    public Task Ping() => RespondAsync($"Pong - {Context.Client.Latency} ms");    [SlashCommand("roll", "Roll a die")]    public Task Roll([MinValue(2), MaxValue(100)] int sides = 6)        => RespondAsync($"You rolled {Random.Shared.Next(1, sides + 1)}");    [SlashCommand("report", "Build a server report")]    public async Task Report()    {        await DeferAsync(ephemeral: true);        var text = await BuildSlowReportAsync();        await FollowupAsync(text, ephemeral: true);    }    private static async Task<string> BuildSlowReportAsync()    {        await Task.Delay(5000);        return "All quiet.";    }}

Три правила модели взаимодействий Discord определяют каждую команду:

  • Отвечайте в течение трёх секунд. Если команде нужно больше - запрос к базе данных, HTTP-вызов, что угодно медленное, - сначала вызовите DeferAsync(). Пользователь увидит «думает», а у вас будет 15 минут, чтобы отправить настоящий ответ через FollowupAsync. Команда, не уложившаяся в три секунды, показывает «The application did not respond», даже если ваш код мгновением позже успешно завершится.
  • Регистрируйте команды, а не просто объявляйте их. Discord должен знать о существовании команды, прежде чем пользователи смогут её увидеть. Пример регистрирует команды на одном тестовом сервере через RegisterCommandsToGuildAsync, что вступает в силу сразу. Для продакшена регистрируйте глобально через RegisterCommandsGloballyAsync; глобальные изменения исторически появлялись дольше, так что разрабатывайте на одном сервере и переключайтесь при релизе.
  • Не перерегистрируйте команды при каждом запуске без нужды. Регистрация заменяет весь набор команд. Это безвредно, но это вызов API, и бот, который падает в цикле и каждый раз перерегистрирует команды, - один из способов познакомиться с лимитами частоты Discord.

Параметры проверяет Discord, прежде чем они до вас дойдут - MinValue и MaxValue выше соблюдаются в клиенте, - так что метод получает значения в допустимом диапазоне. Кнопки, меню выбора и модальные окна используют обработчики [ComponentInteraction] и [ModalInteraction] в тех же модулях.

Хостинг 24/7#

Gateway-бот сам подключается к Discord; к нему ничего не подключается. Ему не нужны ни порт, ни домен, ни сертификат. Ему нужен процесс, который не падает, а если падает - возвращается, и именно это делает панель хостинга. Общие варианты, от лишнего ПК до VDS, разобраны в статье как держать бота для Discord онлайн 24/7.

На панели с Git-деплоем стартовая команда публикует и запускает бота:

bash
dotnet publish -c Release -o out --disable-build-servers && exec dotnet out/TavernBot.dll

exec делает бота главным процессом, так что сигнал остановки от панели доходит до Generic Host, тот вызывает StopAsync, а он чисто закрывает соединение с gateway. Бот, убитый без этого, ещё какое-то время висит «призраком» в сети, пока Discord не закроет соединение по таймауту.

Что важно на сервере:

  • Пишите логи в консоль. Консольный логгер хоста пишет в stdout, который панель показывает вживую. Разрывы и переподключения к gateway - это нормально; Discord.Net пишет о них в лог и переподключается сам. Бот, который пишет о переподключении каждые несколько секунд, имеет проблему с сетью или токеном.
  • Ожидайте перезапусков. Деплои, обновления и случайные падения перезапускают процесс. Храните всё, что должно пережить перезапуск, - настройки по серверам, данные пользователей, кулдауны, - вне памяти.
  • Держите отдельного тестового бота. Создайте второе приложение в Developer Portal для разработки, со своим токеном, приглашённое только на приватный тестовый сервер. Запуск локальной копии с продакшен-токеном - это как два процесса начинают драться за одну gateway-сессию и как недоделанные команды появляются на серверах вашего сообщества.
  • Необработанные исключения в обработчиках событий. Discord.Net запускает обработчики в собственных задачах и пишет в лог исключения из них, но обработчик, который надолго блокируется, останавливает gateway. Держите обработчики короткими, а медленную работу выносите в фоновую задачу или очередь, как описано в статье worker services и фоновые задачи в .NET.

В RE:NODE тариф C# / .NET деплоит бота из репозитория GitHub, принимает токен как переменную окружения на вкладке Startup и перезапускает процесс, если он завершается. Деплой при push перезапускает только уже работающий сервер, так что бот, которого вы остановили намеренно, остаётся остановленным. Наблюдатель за сбоями считает неожиданные перезапуски: три за час выводят предупреждение на странице сервера и автоматически открывают тикет - так бот, застрявший в цикле падений, не остаётся незамеченным. Выбор размера описан в статье как выбрать тариф для бота Discord.

Память, кэширование и шардинг#

Память бота - это в основном кэш состояния Discord в библиотеке: серверы, каналы, роли и, если вы их запросите, участники и сообщения.

Настройка (DiscordSocketConfig)По умолчаниюВлияние на память
MessageCacheSize0Сообщения, хранимые на канал; 0 - не хранить ни одного
AlwaysDownloadUsersfalseЗагружать при запуске всех участников всех серверов
GatewayIntentsнабор непривилегированныхМеньше intents - меньше событий и меньше кэша

Бот на нескольких серверах с настройками по умолчанию спокойно укладывается в 150 МБ. Включение GuildMembers с AlwaysDownloadUsers на серверах с десятками тысяч участников - вот что доводит бота до гигабайтов. Загружайте участников только для тех серверов и в те моменты, когда они нужны.

Проект worker по умолчанию использует Workstation GC, который держит память ниже, чем это делало бы приложение ASP.NET Core; разница объясняется в статье память и сборка мусора в .NET. В RE:NODE процесс, достигший лимита памяти тарифа, останавливается и перезапускается начисто, а не уходит в подкачку, так что неограниченно растущий кэш проявляется периодическими перезапусками, а не медленным ботом.

Шардинг разбивает gateway-соединение на несколько сессий, каждая из которых обслуживает часть серверов. Discord требует его начиная с 2500 серверов. Discord.Net предоставляет для этого DiscordShardedClient. Ниже этого порога одно соединение проще и вполне достаточно.

Где бот хранит данные#

Всё важное должно переживать процесс. Варианты в порядке возрастания усилий:

  • JSON-файл для нескольких настроек. Годится, пока две команды не начнут писать его одновременно; используйте блокировку.
  • SQLite через EF Core или Microsoft.Data.Sqlite. Один файл, никакого сервера, настоящие транзакции. Держите файл вне папки, которую заменяет деплой.
  • Сервер баз данных, как только данные читает больше одного процесса или как только вы хотите, чтобы backup делали за вас. Тариф для приложений здесь включает два слота баз данных со сгенерированными учётными данными; провайдеры разобраны в статье .NET с PostgreSQL, MySQL или SQL Server.

Кулдауны и лимиты частоты на пользователя могут жить в памяти, если их потеря при перезапуске приемлема, а для бота она обычно приемлема.

Разбор проблем#

Бот в сети, но slash-команды не появляются. Они не были зарегистрированы, были зарегистрированы на другом сервере, или в приглашении не было scope applications.commands. Пригласите бота заново с обоими scope и проверьте, что вызов регистрации в обработчике Ready прошёл без ошибки в логе.

«The application did not respond». Команда выполнялась дольше трёх секунд без отложенного ответа или выбросила исключение до ответа. Посмотрите лог, затем добавьте DeferAsync() ко всему, что выполняет ввод-вывод.

Текст сообщений пуст. Intent MessageContent отсутствует в коде, в портале или и там и там. Либо перенесите функцию на slash-команду.

«Authentication failed» или 401 при входе. Токен неверный, содержит лишнюю кавычку или пробел из переменной окружения, или был сброшен. Скопируйте его заново.

Бот постоянно отключается. Запущены два процесса с одним и тем же токеном - часто забытая локальная копия и копия на сервере, - и Discord раз за разом закрывает одну из них. Остановите локальную.

FAQ#

Какая библиотека лучше всего для первого бота на C#?

Discord.Net, в основном из-за объёма доступных примеров и ответов. NetCord - сильный выбор для нового бота на актуальном .NET, если вы уверенно обращаетесь с Generic Host.

Нужен ли боту для Discord публичный IP или открытый порт?

Gateway-боту, который подключается наружу, - нет. Публичный HTTPS-эндпоинт нужен только боту, который получает взаимодействия по HTTP вместо gateway; на тарифе для приложений его может предоставить слот прокси с автоматическим сертификатом.

Сколько памяти нужно боту для Discord на C#?

Обычно от 60 до 200 МБ для бота на умеренном числе серверов с кэшированием по умолчанию. Меняет это загрузка полных списков участников больших серверов.

Можно ли запустить бота и веб-панель в одном приложении?

Да. Используйте приложение ASP.NET Core и зарегистрируйте в нём бота как hosted service; они будут делить конфигурацию и базу данных. Веб-панели тогда понадобятся порт и слот прокси, а бот сохранит своё исходящее gateway-соединение.

Почему мой бот ещё какое-то время в сети после остановки?

Его убили, а не остановили, поэтому он так и не закрыл свою gateway-сессию, и Discord ждёт, пока соединение закроется по таймауту. Убедитесь, что стартовая команда использует exec, а хост вызывает StopAsync.


Комментарии

Полностью анонимно: без аккаунта, без почты, без cookie. Мы храним имя, которое вы ввели, текст и время - больше ничего. Количество ссылок ограничено, разметка не отображается.

0/2000