Как сделать Discord-бота: обзор и руководство

Опубликовано: 2022-03-11

Discord — это платформа для обмена сообщениями в реальном времени, которая позиционирует себя как «универсальный голосовой и текстовый чат для геймеров». Благодаря удобному интерфейсу, простоте использования и обширным функциям Discord быстро растет и становится все более популярным даже среди тех, кто мало интересуется видеоиграми. В период с мая 2017 года по май 2018 года его пользовательская база увеличилась с 45 миллионов пользователей до более чем 130 миллионов, что более чем в два раза превышает количество ежедневных пользователей Slack.

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

В этом руководстве по ботам Discord мы начнем с обсуждения пользовательского интерфейса Discord и его API-интерфейсов REST и WebSocket для ботов, а затем перейдем к руководству, в котором мы напишем простого бота Discord на JavaScript. Наконец, мы услышим от разработчика, по некоторым показателям, самого популярного бота Discord, и о его опыте разработки и поддержки его значительной инфраструктуры и кодовой базы.

Пользовательский интерфейс раздора

Прежде чем мы обсудим технические детали, важно понять, как пользователь взаимодействует с Discord и как Discord представляет себя пользователям. То, как он представляется ботам, концептуально похоже (но, конечно, не визуально). На самом деле официальные приложения Discord построены на тех же API, что и боты. Технически возможно запустить бота внутри учетной записи обычного пользователя с небольшими изменениями, но это запрещено условиями обслуживания Discord. Боты должны работать в аккаунтах ботов.

Вот взгляд на версию браузера 1 приложения Discord, работающего внутри Chrome.

Веб-интерфейс раздора

1 Пользовательский интерфейс Discord для настольного приложения практически такой же, как и для веб-приложения, поставляемого вместе с Electron. Приложение iOS создано с помощью React Native. Приложение для Android представляет собой собственный код Java для Android.

Давайте сломаем это.

1. Список серверов

Слева находится список серверов, на которых я состою. Если вы знакомы со Slack, сервер аналогичен рабочей области Slack и представляет собой группу пользователей, которые могут взаимодействовать друг с другом в рамках одного или нескольких каналов на сервере. Сервер управляется его создателем и/или любым другим персоналом, который они выбирают и которому делегируют обязанности. Создатель и/или персонал определяют правила, структуру каналов на сервере и управляют пользователями.

В моем случае сервер API Discord находится вверху моего списка серверов. Это отличное место, чтобы получить помощь и поговорить с другими разработчиками. Ниже находится созданный мной сервер под названием Test . Мы будем тестировать бота, которого мы создадим позже. Ниже находится кнопка для создания нового сервера. Любой может создать сервер в несколько кликов.

Обратите внимание, что хотя в пользовательском интерфейсе Discord используется термин « Сервер », в документации для разработчиков и API используется термин « Гильдия ». Как только мы перейдем к разговору о технических темах, мы переключимся на разговор о гильдиях . Эти два термина взаимозаменяемы.

2. Список каналов

Справа от списка серверов находится список каналов для сервера, который я сейчас просматриваю (в данном случае это сервер Discord API). Каналы могут быть разбиты на произвольное количество категорий. На сервере Discord API категории включают ИНФОРМАЦИЮ, ОБЩИЕ и БИБЛИОТЕКИ, как показано. Каждый канал функционирует как чат, где пользователи могут обсуждать любую тему, которой посвящен канал. Канал, который мы сейчас просматриваем (информация), имеет более светлый фон. Каналы, в которых есть новые сообщения с момента их последнего просмотра, имеют белый цвет текста.

3. Просмотр канала

Это представление канала, где мы можем видеть, о чем говорили пользователи на канале, который мы просматриваем в данный момент. Здесь мы видим одно сообщение, видимое только частично. Это список ссылок на серверы поддержки для отдельных библиотек ботов Discord. Администраторы сервера настроили этот канал так, что обычные пользователи вроде меня не могут отправлять в него сообщения. Администрация использует этот канал как доску объявлений для размещения важной информации, где ее легко увидеть и она не будет заглушена чатом.

4. Список пользователей

Справа находится список пользователей, которые в настоящее время находятся в сети на этом сервере. Пользователи организованы в разные категории, и их имена окрашены в разные цвета. Это результат ролей , которые у них есть. Роль описывает, в какой категории (если есть) должен отображаться пользователь, какого цвета должно быть его имя и какие разрешения у него есть на сервере. У пользователя может быть более одной роли (и очень часто так и есть), и есть некоторая математика приоритета, которая определяет, что произойдет в этом случае. Как минимум, у каждого пользователя есть роль @everyone. Другие роли создаются и назначаются персоналом сервера.

5. Ввод текста

Это текстовый ввод, где я мог бы печатать и отправлять сообщения, если бы мне было позволено. Поскольку у меня нет разрешения на отправку сообщений в этом канале, я не могу печатать здесь.

6. Пользователь

Это текущий пользователь. Я установил свое имя пользователя «Я», чтобы не запутаться, и потому что я ужасно выбираю имена. Под моим именем пользователя находится число (#9484), которое является моим дискриминатором. Может быть много других пользователей с именем «Я», но я единственный «Я № 9484». Я также могу установить себе псевдоним для каждого сервера, чтобы меня знали под разными именами на разных серверах.

Это основные части пользовательского интерфейса Discord, но есть и многое другое. Начать пользоваться Discord легко даже без создания учетной записи, так что не стесняйтесь потратить минуту, чтобы покопаться. Вы можете войти в Discord, посетив домашнюю страницу Discord, нажав «открыть Discord в браузере», выбрав имя пользователя и, возможно, сыграв один или два освежающих раунда «нажмите на картинку автобуса».

Дискорд API

API Discord состоит из двух отдельных частей: WebSocket и REST API. Вообще говоря, WebSocket API используется для получения событий от Discord в режиме реального времени, а REST API используется для выполнения действий внутри Discord.

Как сделать цикл общения с ботом в дискорде

API-интерфейс веб-сокета

API WebSocket используется для получения событий от Discord, включая создание и удаление сообщений, события удаления/блокировки пользователя, обновления разрешений пользователя и многое другое. С другой стороны, связь бота с API WebSocket более ограничена. Бот использует API-интерфейс WebSocket для запроса соединения, идентификации себя, пульса, управления голосовыми соединениями и выполнения еще нескольких фундаментальных действий. Вы можете прочитать более подробную информацию в документации шлюза Discord (одно подключение к API WebSocket называется шлюзом). Для выполнения других действий используется REST API.

События из API WebSocket содержат полезную нагрузку, включая информацию, которая зависит от типа события. Например, все события Message Create будут сопровождаться пользовательским объектом, представляющим автора сообщения. Однако сам по себе пользовательский объект не содержит всей информации, которую необходимо знать о пользователе. Например, отсутствует информация о разрешениях пользователя. Если вам нужна дополнительная информация, вы можете запросить ее у REST API, но по причинам, объясненным далее в следующем разделе, вам обычно следует обращаться к кешу, который вы должны были создать из полезных данных, полученных от предыдущих событий. Существует ряд событий, которые доставляют полезные данные, относящиеся к разрешениям пользователя, включая, помимо прочего, создание гильдии, обновление роли гильдии и обновление канала .

Бот может присутствовать максимум в 2500 гильдиях на одно соединение WebSocket. Чтобы позволить боту присутствовать в большем количестве гильдий, бот должен реализовать сегментирование и открыть несколько отдельных соединений WebSocket с Discord. Если ваш бот работает внутри одного процесса на одном узле, это просто дополнительная сложность для вас, которая может показаться ненужной. Но если ваш бот очень популярен и его серверная часть должна быть распределена по отдельным узлам, поддержка сегментирования Discord сделает это намного проще, чем в противном случае.

REST-API

Discord REST API используется ботами для выполнения большинства действий, таких как отправка сообщений, удаление/блокировка пользователей и обновление разрешений пользователей (в целом аналогично событиям, полученным от WebSocket API). REST API также можно использовать для запроса информации; однако вместо этого боты в основном полагаются на события из API WebSocket и кэшируют информацию, полученную из событий WebSocket.

На это есть несколько причин. Например, запрос REST API для получения информации о пользователе каждый раз, когда получено событие Message Create , не масштабируется из-за ограничений скорости REST API. Кроме того, в большинстве случаев это избыточно, поскольку WebSocket API предоставляет необходимую информацию, и вы должны иметь ее в своем кеше.

Однако есть некоторые исключения, и иногда вам может понадобиться информация, которой нет в вашем кэше. Когда бот первоначально подключается к шлюзу WebSocket, событие Ready и одно событие Guild Create для каждой гильдии, в которой бот присутствует в этом сегменте, изначально отправляются боту, чтобы он мог заполнить свой кэш текущим состоянием. События создания гильдии для густонаселенных гильдий содержат информацию только об онлайн-пользователях. Если вашему боту необходимо получить информацию об автономном пользователе, соответствующая информация может отсутствовать в вашем кеше. В этом случае имеет смысл сделать запрос к REST API. Или, если вам часто требуется получать информацию об офлайн-пользователях, вы можете вместо этого отправить код операции Request Guild Members в WebSocket API, чтобы запросить офлайн-членов гильдии.

Другое исключение — если ваше приложение вообще не подключено к WebSocket API. Например, если у вашего бота есть веб-панель управления, в которую пользователи могут входить и изменять настройки бота на своем сервере. Веб-панель может работать в отдельном процессе без каких-либо подключений к WebSocket API и без кеша данных из Discord. Возможно, потребуется лишь время от времени делать несколько запросов REST API. В таком сценарии имеет смысл полагаться на REST API для получения необходимой информации.

Обертки API

Хотя всегда полезно иметь некоторое представление о каждом уровне вашего технологического стека, прямое использование Discord WebSocket и REST API отнимает много времени, подвержено ошибкам, как правило, не нужно и фактически опасно.

Discord предоставляет список официально проверенных библиотек и предупреждает, что:

Использование пользовательских реализаций или несовместимых библиотек, которые злоупотребляют API или вызывают чрезмерные ограничения скорости, могут привести к постоянному бану.

Библиотеки, официально проверенные Discord, как правило, являются зрелыми, хорошо документированными и полностью охватывают API Discord. У большинства разработчиков ботов никогда не будет веских причин для разработки собственной реализации, кроме как из любопытства или храбрости!

В настоящее время официально проверенные библиотеки включают реализации для Crystal, C#, D, Go, Java, JavaScript, Lua, Nim, PHP, Python, Ruby, Rust и Swift. Для выбранного вами языка может быть две или более разных библиотек. Выбор того, что использовать, может быть трудным решением. В дополнение к просмотру соответствующей документации вы можете присоединиться к неофициальному серверу API Discord и почувствовать, какое сообщество стоит за каждой библиотекой.

Как сделать Discord-бота

Давайте приступим к делу. Мы собираемся создать бота Discord, который зависает на нашем сервере и прослушивает веб-хуки от Ko-fi. Ko-fi — это сервис, который позволяет вам легко принимать пожертвования на свой счет PayPal. Там очень просто настроить веб-хуки, в отличие от PayPal, где вам нужно иметь бизнес-аккаунт, поэтому он отлично подходит для демонстрационных целей или обработки небольших пожертвований.

Когда пользователь пожертвует 10 долларов или более, бот назначит ему роль Premium Member , которая изменит цвет его имени и переместит его в верхнюю часть списка онлайн-пользователей. Для этого проекта мы будем использовать Node.js и библиотеку API Discord под названием Eris (ссылка на документацию: https://abal.moe/Eris/). Eris — не единственная библиотека JavaScript. Вместо этого вы можете выбрать discord.js. Код, который мы напишем, будет очень похож в любом случае.

Кроме того, Patreon, еще один обработчик пожертвований, предоставляет официального бота Discord и поддерживает настройку ролей Discord в качестве преимуществ вкладчика. Мы собираемся реализовать нечто подобное, но, конечно, более простое.

Код для каждого шага руководства доступен на GitHub (https://github.com/mistval/premium_bot). Некоторые из шагов, показанных в этом посте, для краткости опускают неизмененный код, поэтому следуйте предоставленным ссылкам на GitHub, если вы считаете, что можете что-то упустить.

Создание учетной записи бота

Прежде чем мы сможем начать писать код, нам нужна учетная запись бота. Прежде чем мы сможем создать учетную запись бота, нам нужна учетная запись пользователя. Чтобы создать учетную запись пользователя, следуйте инструкциям здесь.

Затем, чтобы создать учетную запись бота, мы:

1) Создайте приложение на портале разработчика.

скриншот портала разработчиков

2) Заполните некоторые основные сведения о приложении (обратите внимание на показанный здесь идентификатор клиента — он понадобится нам позже).

скриншот заполнения основных данных

3) Добавить пользователя-бота, подключенного к приложению.

скриншот добавления пользователя бота

4) Выключите переключатель PUBLIC BOT и обратите внимание на показанный токен бота (он нам понадобится позже). Если вы когда-нибудь утечете свой токен бота, например, опубликовав его в виде изображения в сообщении блога Toptal, обязательно немедленно восстановите его. Любой, кто владеет токеном вашего бота, может контролировать учетную запись вашего бота и создавать потенциально серьезные и постоянные проблемы для вас и ваших пользователей.

скриншот "появился дикий бот"

5) Добавьте бота в свою тестовую гильдию. Чтобы добавить бота в гильдию, замените его идентификатор клиента (показанный ранее) на следующий URI и перейдите к нему в браузере.

https://discordapp.com/api/oauth2/authorize?scope=bot&client_id=XXX

Добавьте бота в свою тестовую гильдию

После нажатия авторизоваться бот теперь в моей тестовой гильдии и я вижу его в списке пользователей. Он не в сети, но мы скоро это исправим.

Создание проекта

Предполагая, что у вас установлен Node.js, создайте проект и установите Eris (библиотеку ботов, которую мы будем использовать), Express (инфраструктуру веб-приложений, которую мы будем использовать для создания прослушивателя веб-перехватчиков) и body-parser (для разбора тел веб-перехватчиков). ).

 mkdir premium_bot cd premium_bot npm init npm install eris express body-parser

Получение бота в сети и его отзывчивость

Начнем с детских шагов. Сначала мы просто подключим бота к сети и ответим нам. Мы можем сделать это в 10-20 строках кода. Внутри нового файла bot.js нам нужно создать экземпляр Eris Client, передать ему наш токен бота (полученный, когда мы создали бот-приложение выше), подписаться на некоторые события в экземпляре Client и указать ему подключиться к Discord. . В демонстрационных целях мы жестко закодируем наш токен бота в файл bot.js, но рекомендуется создать отдельный файл конфигурации и исключить его из системы контроля версий.

(ссылка на код GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step1.js)

 const eris = require('eris'); // Create a Client instance with our bot token. const bot = new eris.Client('my_token'); // When the bot is connected and ready, log to console. bot.on('ready', () => { console.log('Connected and ready.'); }); // Every time a message is sent anywhere the bot is present, // this event will fire and we will check if the bot was mentioned. // If it was, the bot will attempt to respond with "Present". bot.on('messageCreate', async (msg) => { const botWasMentioned = msg.mentions.find( mentionedUser => mentionedUser.id === bot.user.id, ); if (botWasMentioned) { try { await msg.channel.createMessage('Present'); } catch (err) { // There are various reasons why sending a message may fail. // The API might time out or choke and return a 5xx status, // or the bot may not have permission to send the // message (403 status). console.warn('Failed to respond to mention.'); console.warn(err); } } }); bot.on('error', err => { console.warn(err); }); bot.connect();

Если все пойдет хорошо, когда вы запустите этот код со своим токеном бота, Connected and ready. будет напечатано на консоли, и вы увидите, как ваш бот подключается к сети на тестовом сервере. Вы можете упомянуть 2 своего бота, либо щелкнув его правой кнопкой мыши и выбрав «Упомянуть», либо введя его имя, которому предшествует @. Бот должен ответить, сказав «Присутствует».

Ваш бот присутствует

2 Упоминание — это способ привлечь внимание другого пользователя, даже если он отсутствует. Обычный пользователь при упоминании будет уведомлен уведомлением на рабочем столе, мобильным push-уведомлением и/или маленьким красным значком, появляющимся над значком Discord на панели задач. Способ(ы) уведомления пользователя зависит от его настроек и состояния в сети. Боты, с другой стороны, не получают никаких специальных уведомлений, когда они упоминаются. Они получают обычное событие создания сообщения, как и для любого другого сообщения, и могут проверять упоминания, прикрепленные к событию, чтобы определить, были ли они упомянуты.

Запись платежной команды

Теперь, когда мы знаем, что можем подключить бота к сети, давайте избавимся от нашего текущего обработчика событий Message Create и создадим новый, который позволит нам сообщить боту, что мы получили платеж от пользователя.

Чтобы сообщить боту о платеже, мы выдадим команду, которая выглядит так:

 pb!addpayment @user_mention payment_amount

Например, pb!addpayment @Me 10.00 для записи платежа в размере 10 долларов США, сделанного мной.

ПБ! часть называется префиксом команды. Рекомендуется выбрать префикс, с которого должны начинаться все команды для вашего бота. Это создает пространство имен для ботов и помогает избежать конфликтов с другими ботами. Большинство ботов включают команду помощи, но представьте себе беспорядок, если бы у вас было десять ботов в вашей гильдии, и все они откликнулись на помощь ! Использование пб! в качестве префикса не является надежным решением, так как могут быть другие боты, которые также используют тот же префикс. Большинство популярных ботов позволяют настраивать свой префикс для каждой гильдии, чтобы предотвратить конфликты. Другой вариант — использовать собственное упоминание бота в качестве его префикса, хотя это делает выдачу команд более подробными.

(ссылка на код GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step2.js)

 const eris = require('eris'); const PREFIX = 'pb!'; const bot = new eris.Client('my_token'); const commandHandlerForCommandName = {}; commandHandlerForCommandName['addpayment'] = (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); // TODO: Handle invalid command arguments, such as: // 1. No mention or invalid mention. // 2. No amount or invalid amount. return msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`); }; bot.on('messageCreate', async (msg) => { const content = msg.content; // Ignore any messages sent as direct messages. // The bot will only accept commands issued in // a guild. if (!msg.channel.guild) { return; } // Ignore any message that doesn't start with the correct prefix. if (!content.startsWith(PREFIX)) { return; } // Extract the parts of the command and the command name const parts = content.split(' ').map(s => s.trim()).filter(s => s); const commandName = parts[0].substr(PREFIX.length); // Get the appropriate handler for the command, if there is one. const commandHandler = commandHandlerForCommandName[commandName]; if (!commandHandler) { return; } // Separate the command arguments from the command prefix and command name. const args = parts.slice(1); try { // Execute the command. await commandHandler(msg, args); } catch (err) { console.warn('Error handling command'); console.warn(err); } }); bot.on('error', err => { console.warn(err); }); bot.connect();

Давай попробуем.

Взаимодействие с ботом

Мы не только заставили бота реагировать на команду pb!addpayment , но и создали общий шаблон для обработки команд. Мы можем добавить больше команд, просто добавив больше обработчиков в словарь commandHandlerForCommandName . У нас есть задатки простой командной среды. Обработка команд — настолько фундаментальная часть создания бота, что многие люди написали командные среды с открытым исходным кодом, которые вы могли бы использовать вместо того, чтобы писать свои собственные. Платформы команд часто позволяют указать время восстановления, необходимые разрешения пользователя, псевдонимы команд, описания команд и примеры использования (для автоматически созданной команды справки) и многое другое. Eris поставляется со встроенной структурой команд.

Говоря о разрешениях, у нашего бота есть небольшая проблема с безопасностью. Любой может выполнить команду addpayment . Давайте ограничим его, чтобы им мог пользоваться только владелец бота. Мы проведем рефакторинг словаря commandHandlerForCommandName , чтобы он содержал объекты JavaScript в качестве значений. Эти объекты будут содержать свойство execute с обработчиком команд и свойство botOwnerOnly с логическим значением. Мы также жестко закодируем наш идентификатор пользователя в разделе констант бота, чтобы он знал, кто его владелец. Вы можете найти свой идентификатор пользователя, включив режим разработчика в настройках Discord, затем щелкнув правой кнопкой мыши свое имя пользователя и выбрав «Копировать идентификатор».

(ссылка на код GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step3.js)

 const eris = require('eris'); const PREFIX = 'pb!'; const BOT_OWNER_; const bot = new eris.Client('my_token'); const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); // TODO: Handle invalid command arguments, such as: // 1. No mention or invalid mention. // 2. No amount or invalid amount. return msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`); }, }; bot.on('messageCreate', async (msg) => { try { const content = msg.content; // Ignore any messages sent as direct messages. // The bot will only accept commands issued in // a guild. if (!msg.channel.guild) { return; } // Ignore any message that doesn't start with the correct prefix. if (!content.startsWith(PREFIX)) { return; } // Extract the parts and name of the command const parts = content.split(' ').map(s => s.trim()).filter(s => s); const commandName = parts[0].substr(PREFIX.length); // Get the requested command, if there is one. const command = commandForName[commandName]; if (!command) { return; } // If this command is only for the bot owner, refuse // to execute it for any other user. const authorIsBotOwner = msg.author.id === BOT_OWNER_ID; if (command.botOwnerOnly && !authorIsBotOwner) { return await msg.channel.createMessage('Hey, only my owner can issue that command!'); } // Separate the command arguments from the command prefix and name. const args = parts.slice(1); // Execute the command. await command.execute(msg, args); } catch (err) { console.warn('Error handling message create event'); console.warn(err); } }); bot.on('error', err => { console.warn(err); }); bot.connect();

Теперь бот гневно откажется выполнить команду addpayment , если ее попытается выполнить кто-либо, кроме владельца бота.

Далее давайте назначим боту роль Premium Member любому, кто пожертвует десять долларов или больше. В верхней части файла bot.js:

(ссылка на код GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step4.js)

 const eris = require('eris'); const PREFIX = 'pb!'; const BOT_OWNER_; const PREMIUM_CUTOFF = 10; const bot = new eris.Client('my_token'); const premiumRole = { name: 'Premium Member', color: 0x6aa84f, hoist: true, // Show users with this role in their own section of the member list. }; async function updateMemberRoleForDonation(guild, member, donationAmount) { // If the user donated more than $10, give them the premium role. if (guild && member && donationAmount >= PREMIUM_CUTOFF) { // Get the role, or if it doesn't exist, create it. let role = Array.from(guild.roles.values()) .find(role => role.name === premiumRole.name); if (!role) { role = await guild.createRole(premiumRole); } // Add the role to the user, along with an explanation // for the guild log (the "audit log"). return member.addRole(role.id, 'Donated $10 or more.'); } } const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); const guild = msg.channel.guild; const userId = mention.replace(/<@(.*?)>/, (match, group1) => group1); const member = guild.members.get(userId); // TODO: Handle invalid command arguments, such as: // 1. No mention or invalid mention. // 2. No amount or invalid amount. return Promise.all([ msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`), updateMemberRoleForDonation(guild, member, amount), ]); }, };

Теперь я могу попробовать сказать pb!addpayment @Me 10.00 , и бот должен назначить мне роль Premium Member .

К сожалению, в консоли появляется ошибка «Отсутствуют разрешения».

 DiscordRESTError: DiscordRESTError [50013]: Missing Permissions index.js:85 code:50013

У бота нет разрешения на управление ролями в тестовой гильдии, поэтому он не может создавать или назначать роли. Мы могли бы дать боту привилегию администратора, и у нас никогда больше не было бы такой проблемы, но, как и в любой системе, лучше всего предоставить пользователю (или в данном случае боту) только минимальные привилегии, которые ему требуются.

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

Начать управление ролями

Создать новую роль

Теперь, когда я снова пытаюсь выполнить команду, роль создается и назначается мне, и у меня есть причудливый цвет имени и особая позиция в списке участников.

Назначается новая роль

В обработчике команды у нас есть комментарий TODO, предлагающий проверить недопустимые аргументы. Давайте позаботимся об этом сейчас.

(ссылка на код GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step5.js)

 const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); const guild = msg.channel.guild; const userId = mention.replace(/<@(.*?)>/, (match, group1) => group1); const member = guild.members.get(userId); const userIsInGuild = !!member; if (!userIsInGuild) { return msg.channel.createMessage('User not found in this guild.'); } const amountIsValid = amount && !Number.isNaN(amount); if (!amountIsValid) { return msg.channel.createMessage('Invalid donation amount'); } return Promise.all([ msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`), updateMemberRoleForDonation(guild, member, amount), ]); }, };

Вот полный код:

(ссылка на код GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step5.js)

 const eris = require('eris'); const PREFIX = 'pb!'; const BOT_OWNER_; const PREMIUM_CUTOFF = 10; const bot = new eris.Client('my_token'); const premiumRole = { name: 'Premium Member', color: 0x6aa84f, hoist: true, // Show users with this role in their own section of the member list. }; async function updateMemberRoleForDonation(guild, member, donationAmount) { // If the user donated more than $10, give them the premium role. if (guild && member && donationAmount >= PREMIUM_CUTOFF) { // Get the role, or if it doesn't exist, create it. let role = Array.from(guild.roles.values()) .find(role => role.name === premiumRole.name); if (!role) { role = await guild.createRole(premiumRole); } // Add the role to the user, along with an explanation // for the guild log (the "audit log"). return member.addRole(role.id, 'Donated $10 or more.'); } } const commandForName = {}; commandForName['addpayment'] = { botOwnerOnly: true, execute: (msg, args) => { const mention = args[0]; const amount = parseFloat(args[1]); const guild = msg.channel.guild; const userId = mention.replace(/<@(.*?)>/, (match, group1) => group1); const member = guild.members.get(userId); const userIsInGuild = !!member; if (!userIsInGuild) { return msg.channel.createMessage('User not found in this guild.'); } const amountIsValid = amount && !Number.isNaN(amount); if (!amountIsValid) { return msg.channel.createMessage('Invalid donation amount'); } return Promise.all([ msg.channel.createMessage(`${mention} paid $${amount.toFixed(2)}`), updateMemberRoleForDonation(guild, member, amount), ]); }, }; bot.on('messageCreate', async (msg) => { try { const content = msg.content; // Ignore any messages sent as direct messages. // The bot will only accept commands issued in // a guild. if (!msg.channel.guild) { return; } // Ignore any message that doesn't start with the correct prefix. if (!content.startsWith(PREFIX)) { return; } // Extract the parts and name of the command const parts = content.split(' ').map(s => s.trim()).filter(s => s); const commandName = parts[0].substr(PREFIX.length); // Get the requested command, if there is one. const command = commandForName[commandName]; if (!command) { return; } // If this command is only for the bot owner, refuse // to execute it for any other user. const authorIsBotOwner = msg.author.id === BOT_OWNER_ID; if (command.botOwnerOnly && !authorIsBotOwner) { return await msg.channel.createMessage('Hey, only my owner can issue that command!'); } // Separate the command arguments from the command prefix and name. const args = parts.slice(1); // Execute the command. await command.execute(msg, args); } catch (err) { console.warn('Error handling message create event'); console.warn(err); } }); bot.on('error', err => { console.warn(err); }); bot.connect();

Это должно дать вам хорошее представление о том, как создать бота Discord. Теперь посмотрим, как интегрировать бота с Ko-fi. Если хотите, вы можете создать веб-перехватчик на панели управления в Ko-fi, убедиться, что ваш маршрутизатор настроен на переадресацию порта 80, и отправить себе настоящие тестовые веб-перехватчики. Но я просто собираюсь использовать Postman для имитации запросов.

Вебхуки от Ko-fi доставляют полезные данные, которые выглядят следующим образом:

 data: { "message_id":"3a1fac0c-f960-4506-a60e-824979a74e74", "timestamp":"2017-08-21T13:04:30.7296166Z", "type":"Donation","from_name":"John Smith", "message":"Good luck with the integration!", "amount":"3.00", "url":"https://ko-fi.com" }

Давайте создадим новый исходный файл с именем webhook_listener.js и воспользуемся Express для прослушивания веб-перехватчиков. У нас будет только один экспресс-маршрут, и он предназначен для демонстрационных целей, поэтому мы не будем слишком беспокоиться об использовании идиоматической структуры каталогов. Мы просто поместим всю логику веб-сервера в один файл.

(ссылка на код GitHub: https://github.com/mistval/premium_bot/blob/master/src/webhook_listener_step6.js)

 const express = require('express'); const app = express(); const PORT = process.env.PORT || 80; class WebhookListener { listen() { app.get('/kofi', (req, res) => { res.send('Hello'); }); app.listen(PORT); } } const listener = new WebhookListener(); listener.listen(); module.exports = listener;

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

(ссылка на код GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step6.js)

 const eris = require('eris'); const webhookListener = require('./webhook_listener.js');

После запуска бота вы должны увидеть «Hello» при переходе по адресу http://localhost/kofi в браузере.

Теперь давайте сделаем так, чтобы WebhookListener обрабатывал данные из веб-перехватчика и генерировал событие. И теперь, когда мы проверили, что наш браузер может получить доступ к маршруту, давайте изменим маршрут на маршрут POST, поскольку веб-перехватчик от Ko-fi будет запросом POST.

(ссылка на код GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step7.js)

 const express = require('express'); const bodyParser = require('body-parser'); const EventEmitter = require('events'); const PORT = process.env.PORT || 80; const app = express(); app.use(bodyParser.json()); class WebhookListener extends EventEmitter { listen() { app.post('/kofi', (req, res) => { const data = req.body.data; const { message, timestamp } = data; const amount = parseFloat(data.amount); const senderName = data.from_name; const paymentId = data.message_id; const paymentSource = 'Ko-fi'; // The OK is just for us to see in Postman. Ko-fi doesn't care // about the response body, it just wants a 200. res.send({ status: 'OK' }); this.emit( 'donation', paymentSource, paymentId, timestamp, amount, senderName, message, ); }); app.listen(PORT); } } const listener = new WebhookListener(); listener.listen(); module.exports = listener;

Next we need to have the bot listen for the event, decide which user donated, and assign them a role. To decide which user donated, we'll try to find a user whose username is a substring of the message received from Ko-fi. Donors must be instructed to provide their username (with the discriminator) in the message than they write when they make their donation.

At the bottom of bot.js:

(GitHub code link: https://github.com/mistval/premium_bot/blob/master/src/bot_step7.js)

 function findUserInString(str) { const lowercaseStr = str.toLowerCase(); // Look for a matching username in the form of username#discriminator. const user = bot.users.find( user => lowercaseStr.indexOf(`${user.username.toLowerCase()}#${user.discriminator}`) !== -1, ); return user; } async function onDonation( paymentSource, paymentId, timestamp, amount, senderName, message, ) { try { const user = findUserInString(message); const guild = user ? bot.guilds.find(guild => guild.members.has(user.id)) : null; const guildMember = guild ? guild.members.get(user.id) : null; return await updateMemberRoleForDonation(guild, guildMember, amount); } catch (err) { console.warn('Error handling donation event.'); console.warn(err); } } webhookListener.on('donation', onDonation); bot.connect();

In the onDonation function, we see two representations of a user: as a User, and as a Member. These both represent the same person, but the Member object contains guild-specific information about the User, such as their roles in the guild and their nickname. Since we want to add a role, we need to use the Member representation of the user. Each User in Discord has one Member representation for each guild that they are in.

Now I can use Postman to test the code.

Testing with Postman

I receive a 200 status code, and I get the role granted to me in the server.

If the message from Ko-fi does not contain a valid username; however, nothing happens. The donor doesn't get a role, and we are not aware that we received an orphaned donation. Let's add a log for logging donations, including donations that can't be attributed to a guild member.

First we need to create a log channel in Discord and get its channel ID. The channel ID can be found using the developer tools, which can be enabled in Discord's settings. Then you can right-click any channel and click “Copy ID.”

The log channel ID should be added to the constants section of bot.js.

(GitHub code link: https://github.com/mistval/premium_bot/blob/master/src/bot_step8.js)

 const LOG_CHANNEL_;

And then we can write a logDonation function.

(GitHub code link: https://github.com/mistval/premium_bot/blob/master/src/bot_step8.js)

 function logDonation(member, donationAmount, paymentSource, paymentId, senderName, message, timestamp) { const isKnownMember = !!member; const memberName = isKnownMember ? `${member.username}#${member.discriminator}` : 'Unknown'; const embedColor = isKnownMember ? 0x00ff00 : 0xff0000; const logMessage = { embed: { title: 'Donation received', color: embedColor, timestamp: timestamp, fields: [ { name: 'Payment Source', value: paymentSource, inline: true }, { name: 'Payment ID', value: paymentId, inline: true }, { name: 'Sender', value: senderName, inline: true }, { name: 'Donor Discord name', value: memberName, inline: true }, { name: 'Donation amount', value: donationAmount.toString(), inline: true }, { name: 'Message', value: message, inline: true }, ], } } bot.createMessage(LOG_CHANNEL_ID, logMessage); }

Now we can update onDonation to call the log function:

 async function onDonation( paymentSource, paymentId, timestamp, amount, senderName, message, ) { try { const user = findUserInString(message); const guild = user ? bot.guilds.find(guild => guild.members.has(user.id)) : null; const guildMember = guild ? guild.members.get(user.id) : null; return await Promise.all([ updateMemberRoleForDonation(guild, guildMember, amount), logDonation(guildMember, amount, paymentSource, paymentId, senderName, message, timestamp), ]); } catch (err) { console.warn('Error updating donor role and logging donation'); console.warn(err); } }

Now I can invoke the webhook again, first with a valid username, and then without one, and I get two nice log messages in the log channel.

Two nice messages

Previously, we were just sending strings to Discord to display as messages. The more complex JavaScript object that we create and send to Discord in the new logDonation function is a special type of message referred to as a rich embed. An embed gives you some scaffolding for making attractive messages like those shown. Only bots can create embeds, users cannot.

Now we are being notified of donations, logging them, and rewarding our supporters. We can also add donations manually with the addpayment command in case a user forgets to specify their username when they donate. Let's call it a day.

The completed code for this tutorial is available on GitHub here https://github.com/mistval/premium_bot

Следующие шаги

We've successfully created a bot that can help us track donations. Is this something we can actually use? Well, perhaps. It covers the basics, but not much more. Here are some shortcomings you might want to think about first:

  1. If a user leaves our guild (or if they weren't even in our guild in the first place), they will lose their Premium Member role, and if they rejoin, they won't get it back. We should store payments by user ID in a database, so if a premium member rejoins, we can give them their role back and maybe send them a nice welcome-back message if we were so inclined.
  2. Paying in installments won't work. If a user sends $5 and then later sends another $5, they won't get a premium role. Similar to the above issue, storing payments in a database and issuing the Premium Member role when the total payments from a user reaches $10 would help here.
  3. It's possible to receive the same webhook more than once, and this bot will record the payment multiple times. If Ko-fi doesn't receive or doesn't properly acknowledge a code 200 response from the webhook listener, it will try to send the webhook again later. Keeping track of payments in a database and ignoring webhooks with the same ID as previously received ones would help here.
  4. Our webhook listener isn't very secure. Anyone could forge a webhook and get a Premium Member role for free. Ko-fi doesn't seem to sign webhooks, so you'll have to rely on either no one knowing your webhook address (bad), or IP whitelisting (a bit better).
  5. The bot is designed to be used in one guild only.

Interview: When a Bot Gets Big

There are over a dozen websites for listing Discord bots and making them available to the public at large, including DiscordBots.org and Discord.Bots.gg. Although Discord bots are mostly the foray of small-time hobbyists, some bots experience tremendous popularity and maintaining them evolves into a complex and demanding job.

By guild-count, Rythm is currently the most widespread bot on Discord. Rythm is a music bot whose specialty is connecting to voice channels in Discord and playing music requested by users. Rythm is currently present in over 2,850,000 guilds containing a sum population of around 90 million users, and at its peak plays audio for around 100,000 simultaneous users in 20,000 separate guilds. Rythm's creator and main developer, ImBursting, kindly agreed to answer a few questions about what it's like to develop and maintain a large-scale bot like Rythm.

Interviewer: Can you tell us a bit about Rythm's high level architecture and how it's hosted?

ImBursting: Rythm is scaled across 9 physical servers, each have 32 cores, 96GB of RAM and a 10gbps connection. These servers are collocated at a data center with help from a small hosting company, GalaxyGate.

I imagine that when you started working on Rythm, you didn't design it to scale anywhere near as much as it has. Can you tell us about about how Rythm started, and its technical evolution over time?

Rythm's first evolution was written in Python, which isn't a very performant language, so around the time we hit 10,000 servers (after many scaling attempts) I realised this was the biggest roadblock and so I began recoding the bot to Java, the reason being Java's audio libraries were a lot more optimised and it was generally a better suited language for such a huge application. After re-coding, performance improved tenfold and kept the issues at bay for a while. And then we hit the 300,000 servers milestone when issues started surfacing again, at which point I realised that more scaling was required since one JVM just wasn't able to handle all that. So we slowly started implementing improvements and major changes like tuning the garbage collector and splitting voice connections onto separate microservices using an open source server called Lavalink. This improved performance quite a bit but the final round of infrastructure was when we split this into 9 seperate clusters to run on 9 physical servers, and made custom gateway and stats microservices to make sure everything ran smoothly like it would on one machine.

I noticed that Rythm has a canary version and you get some help from other developers and staff. I imagine you and your team must put a lot of effort into making sure things are done right. Can you tell us about what processes are involved in updating Rythm?

Rythm canary is the alpha bot we use to test freshly made features and performance improvements before usually deploying them to Rythm 2 to test on a wider scale and then production Rythm. The biggest issue we encounter is really long reboot times due to Discord rate limits, and is the reason I try my best to make sure an update is ready before deciding to push it.

I do get a lot of help from volunteer developers and people who genuinely want to help the community, I want to make sure everything is done correctly and that people will always get their questions answered and get the best support possible which means im constantly on the lookout for new opportunities.

Подведение итогов

Discord's days of being a new kid on the block are past, and it is now one of the largest real-time communication platforms in the world. While Discord bots are largely the foray of small-time hobbyists, we may well see commercial opportunities increase as the population of the service continues to increase. Some companies, like the aforementioned Patreon, have already waded in.

In this article, we saw a high-level overview of Discord's user interface, a high-level overview of its APIs, a complete lesson in Discord bot programming, and we got to hear about what it's like to operate a bot at enterprise scale. I hope you come away interested in the technology and feeling like you understand the fundamentals of how it works.

Chatbots are generally fun, except when their responses to your intricate queries have the intellectual the depth of a cup of water. To ensure a great UX for your users see The Chat Crash - When a Chatbot Fails by the Toptal Design Blog for 5 design problems to avoid.

Связанный: Лучшие практики JS: создание бота Discord с помощью TypeScript и внедрения зависимостей