Cómo hacer un bot de Discord: descripción general y tutorial
Publicado: 2022-03-11Discord es una plataforma de mensajería en tiempo real que se anuncia a sí misma como un "chat de voz y texto todo en uno para jugadores". Debido a su interfaz elegante, facilidad de uso y amplias funciones, Discord ha experimentado un rápido crecimiento y se está volviendo cada vez más popular incluso entre aquellos con poco interés en los videojuegos. Entre mayo de 2017 y mayo de 2018, su base de usuarios se disparó de 45 millones de usuarios a más de 130 millones, con más del doble de usuarios diarios que Slack.
Una de las características más atractivas de Discord desde la perspectiva de un desarrollador de chatbot es su sólido soporte para bots programables que ayudan a integrar Discord con el mundo exterior y brindan a los usuarios una experiencia más atractiva. Los bots son omnipresentes en Discord y brindan una amplia gama de servicios, que incluyen asistencia de moderación, juegos, música, búsquedas en Internet, procesamiento de pagos y más.
En este tutorial de bots de Discord, comenzaremos analizando la interfaz de usuario de Discord y sus API REST y WebSocket para bots antes de pasar a un tutorial en el que escribiremos un bot de Discord simple en JavaScript. Finalmente, escucharemos al desarrollador de, según ciertas métricas, el bot más popular de Discord y sus experiencias en el desarrollo y mantenimiento de su importante infraestructura y base de código.
Interfaz de usuario de discordia
Antes de discutir los detalles técnicos, es importante comprender cómo interactúa un usuario con Discord y cómo se presenta Discord a los usuarios. La forma en que se presenta a los bots es conceptualmente similar (pero, por supuesto, no visual). De hecho, las aplicaciones oficiales de Discord se basan en las mismas API que usan los bots. Es técnicamente posible ejecutar un bot dentro de una cuenta de usuario regular con pocas modificaciones, pero esto está prohibido por los términos de servicio de Discord. Se requiere que los bots se ejecuten en cuentas de bot.
Aquí hay un vistazo a la versión 1 del navegador de la aplicación Discord que se ejecuta dentro de Chrome.
1 La interfaz de usuario de Discord para la aplicación de escritorio es prácticamente la misma que la aplicación web, empaquetada con Electron. La aplicación iOS está construida con React Native. La aplicación de Android es código Java nativo de Android.
Vamos a desglosarlo.
1. Lista de servidores
A la izquierda está la lista de servidores de los que soy miembro. Si está familiarizado con Slack, un servidor es análogo a un espacio de trabajo de Slack y representa un grupo de usuarios que pueden interactuar entre sí dentro de uno o más canales en el servidor. Un servidor es administrado por su creador y/o cualquier personal que seleccionen y elijan delegar responsabilidades. El creador y/o el personal definen las reglas, la estructura de los canales en el servidor y administran los usuarios.
En mi caso, el servidor API de Discord está en la parte superior de mi lista de servidores. Es un gran lugar para obtener ayuda y hablar con otros desarrolladores. Debajo hay un servidor que creé llamado Test . Estaremos probando el bot que creamos más tarde allí. Debajo hay un botón para crear un nuevo servidor. Cualquiera puede crear un servidor con unos pocos clics.
Tenga en cuenta que, si bien el término que se usa en la interfaz de usuario de Discord es Server , el término que se usa en la documentación para desarrolladores y la API es Guild . Una vez que pasemos a hablar de temas técnicos, pasaremos a hablar de gremios . Los dos términos son intercambiables.
2. Lista de canales
Justo a la derecha de la lista de servidores está la lista de canales para el servidor que estoy viendo actualmente (en este caso, el servidor API de Discord). Los canales se pueden dividir en un número arbitrario de categorías. En el servidor API de Discord, las categorías incluyen INFORMACIÓN, GENERAL y LIBRES, como se muestra. Cada canal funciona como una sala de chat donde los usuarios pueden discutir cualquier tema al que se dedique el canal. El canal que estamos viendo actualmente (info) tiene un fondo más claro. Los canales que tienen mensajes nuevos desde la última vez que los vimos tienen un color de texto blanco.
3. Vista de canales
Esta es la vista del canal donde podemos ver de qué han estado hablando los usuarios en el canal que estamos viendo actualmente. Podemos ver un mensaje aquí, solo parcialmente visible. Es una lista de enlaces a servidores de soporte para bibliotecas individuales de bots de Discord. Los administradores del servidor han configurado este canal para que los usuarios habituales como yo no puedan enviar mensajes en él. Los administradores usan este canal como un tablón de anuncios para publicar información importante donde se puede ver fácilmente y no se ahoga por el chat.
4. Lista de usuarios
A la derecha hay una lista de los usuarios actualmente en línea en este servidor. Los usuarios están organizados en diferentes categorías y sus nombres tienen diferentes colores. Esto es el resultado de los roles que tienen. Un rol describe en qué categoría (si corresponde) debe aparecer el usuario, cuál debe ser el color de su nombre y qué permisos tiene en el servidor. Un usuario puede tener más de un rol (y muy a menudo lo tiene), y hay algunas matemáticas de precedencia que determinan lo que sucede en ese caso. Como mínimo, cada usuario tiene el rol @everyone. El personal del servidor crea y asigna otros roles.
5. Entrada de texto
Esta es la entrada de texto donde podría escribir y enviar mensajes, si me lo permitieran. Como no tengo permiso para enviar mensajes en este canal, no puedo escribir aquí.
6. Usuario
Este es el usuario actual. Establecí mi nombre de usuario en "Yo", para ayudar a evitar que me confunda y porque soy terrible para elegir nombres. Debajo de mi nombre de usuario hay un número (#9484) que es mi discriminador. Puede haber muchos otros usuarios llamados "Yo", pero yo soy el único "Yo #9484". También es posible para mí establecer un apodo para mí mismo por servidor, por lo que puedo ser conocido por diferentes nombres en diferentes servidores.
Estas son las partes básicas de la interfaz de usuario de Discord, pero también hay muchas más. Es fácil comenzar a usar Discord incluso sin crear una cuenta, así que siéntete libre de tomarte un minuto para investigar. Puede ingresar a Discord visitando la página de inicio de Discord, haciendo clic en "abrir Discord en un navegador", eligiendo un nombre de usuario y posiblemente jugando una o dos rondas refrescantes de "haga clic en las imágenes del autobús".
La API de discordia
La API de Discord consta de dos piezas separadas: las API WebSocket y REST. En términos generales, la API de WebSocket se usa para recibir eventos de Discord en tiempo real, mientras que la API REST se usa para realizar acciones dentro de Discord.
La API de WebSocket
La API de WebSocket se utiliza para recibir eventos de Discord, incluida la creación de mensajes, la eliminación de mensajes, eventos de expulsión/prohibición de usuarios, actualizaciones de permisos de usuarios y muchos más. La comunicación de un bot a la API de WebSocket, por otro lado, es más limitada. Un bot usa la API de WebSocket para solicitar una conexión, identificarse a sí mismo, realizar latidos, administrar las conexiones de voz y hacer algunas cosas más fundamentales. Puede leer más detalles en la documentación de la puerta de enlace de Discord (una única conexión a la API de WebSocket se denomina puerta de enlace). Para realizar otras acciones, se utiliza la API REST.
Los eventos de la API de WebSocket contienen una carga útil que incluye información que depende del tipo de evento. Por ejemplo, todos los eventos de creación de mensajes estarán acompañados por un objeto de usuario que representa al autor del mensaje. Sin embargo, el objeto de usuario por sí solo no contiene toda la información que hay que saber sobre el usuario. Por ejemplo, no se incluye información sobre los permisos del usuario. Si necesita más información, puede consultar la API de REST, pero por los motivos que se explican más adelante en la siguiente sección, por lo general, debe acceder a la memoria caché que debería haber creado a partir de las cargas útiles recibidas de eventos anteriores. Hay una serie de eventos que entregan cargas útiles relevantes para los permisos de un usuario, incluidos, entre otros, Guild Create , Guild Role Update y Channel Update .
Un bot puede estar presente en un máximo de 2500 gremios por conexión WebSocket. Para permitir que un bot esté presente en más gremios, el bot debe implementar fragmentación y abrir varias conexiones WebSocket independientes a Discord. Si su bot se ejecuta dentro de un solo proceso en un solo nodo, esto es solo una complejidad adicional para usted que puede parecer innecesaria. Pero si su bot es muy popular y necesita tener su back-end distribuido en nodos separados, el soporte de fragmentación de Discord hace que esto sea mucho más fácil de lo que sería de otra manera.
La API REST
Los bots utilizan la API REST de Discord para realizar la mayoría de las acciones, como enviar mensajes, expulsar/prohibir a los usuarios y actualizar los permisos de los usuarios (en términos generales, de forma análoga a los eventos recibidos de la API de WebSocket). La API REST también se puede utilizar para consultar información; sin embargo, los bots se basan principalmente en eventos de la API de WebSocket y almacenan en caché la información recibida de los eventos de WebSocket.
Hay varias razones para esto. Consultar la API REST para obtener información del usuario cada vez que se recibe un evento de creación de mensaje , por ejemplo, no se escala debido a los límites de velocidad de la API REST. También es redundante en la mayoría de los casos, ya que la API de WebSocket entrega la información necesaria y debería tenerla en su caché.
Sin embargo, hay algunas excepciones y, en ocasiones, es posible que necesite información que no está presente en su caché. Cuando un bot se conecta inicialmente a una puerta de enlace WebSocket, un evento Ready y un evento Guild Create por gremio en el que el bot está presente en ese fragmento se envían inicialmente al bot para que pueda llenar su caché con el estado actual. Los eventos Guild Create para gremios muy poblados solo incluyen información sobre usuarios en línea. Si su bot necesita obtener información sobre un usuario fuera de línea, es posible que la información relevante no esté presente en su caché. En este caso, tiene sentido realizar una solicitud a la API REST. O bien, si con frecuencia necesita obtener información sobre usuarios sin conexión, puede optar por enviar un código de operación Solicitar miembros del gremio a la API de WebSocket para solicitar miembros del gremio sin conexión.
Otra excepción es si su aplicación no está conectada a la API de WebSocket en absoluto. Por ejemplo, si su bot tiene un panel web en el que los usuarios pueden iniciar sesión y cambiar la configuración del bot en su servidor. El panel web podría ejecutarse en un proceso separado sin ninguna conexión a la API de WebSocket y sin caché de datos de Discord. Es posible que solo necesite realizar ocasionalmente algunas solicitudes de API REST. En este tipo de escenario, tiene sentido confiar en la API REST para obtener la información que necesita.
Envolturas de API
Si bien siempre es una buena idea tener una cierta comprensión de cada nivel de su pila de tecnología, usar Discord WebSocket y REST API directamente lleva mucho tiempo, es propenso a errores, generalmente innecesario y, de hecho, peligroso.
Discord proporciona una lista seleccionada de bibliotecas examinadas oficialmente y advierte que:
El uso de implementaciones personalizadas o bibliotecas no compatibles que abusan de la API o causan límites de velocidad excesivos puede resultar en una prohibición permanente.
Las bibliotecas examinadas oficialmente por Discord generalmente son maduras, están bien documentadas y cuentan con una cobertura completa de la API de Discord. La mayoría de los desarrolladores de bots nunca tendrán una buena razón para desarrollar una implementación personalizada, ¡excepto por curiosidad o valentía!
En este momento, las bibliotecas examinadas oficialmente incluyen implementaciones para Crystal, C#, D, Go, Java, JavaScript, Lua, Nim, PHP, Python, Ruby, Rust y Swift. Puede haber dos o más bibliotecas diferentes para el idioma de su elección. Elegir cuál usar puede ser una decisión difícil. Además de consultar la documentación respectiva, es posible que desee unirse al servidor API no oficial de Discord y tener una idea de qué tipo de comunidad hay detrás de cada biblioteca.
Cómo hacer un bot de discordia
Vamos a ir al grano. Vamos a crear un bot de Discord que pase el rato en nuestro servidor y escuche los webhooks de Ko-fi. Ko-fi es un servicio que le permite aceptar fácilmente donaciones a su cuenta de PayPal. Es muy simple configurar webhooks allí, a diferencia de PayPal, donde necesita tener una cuenta comercial, por lo que es excelente para fines de demostración o procesamiento de donaciones a pequeña escala.
Cuando un usuario dona $10 o más, el bot le asigna un rol de Premium Member
que cambia el color de su nombre y lo mueve a la parte superior de la lista de usuarios en línea. Para este proyecto, usaremos Node.js y una biblioteca API de Discord llamada Eris (enlace de documentación: https://abal.moe/Eris/). Eris no es la única biblioteca de JavaScript. En su lugar, puede elegir discord.js. El código que escribiremos sería muy similar en ambos sentidos.
Además, Patreon, otro procesador de donaciones, proporciona un bot oficial de Discord y admite la configuración de roles de Discord como beneficios para los contribuyentes. Vamos a implementar algo similar, pero por supuesto más básico.
El código para cada paso del tutorial está disponible en GitHub (https://github.com/mistval/premium_bot). Algunos de los pasos que se muestran en esta publicación omiten el código sin cambios por razones de brevedad, así que sigue los enlaces proporcionados a GitHub si crees que te estás perdiendo algo.
Creación de una cuenta de bot
Antes de que podamos comenzar a escribir código, necesitamos una cuenta de bot. Antes de que podamos crear una cuenta de bot, necesitamos una cuenta de usuario. Para crear una cuenta de usuario, siga las instrucciones aquí.
Luego, para crear una cuenta de bot, nosotros:
1) Cree una aplicación en el portal para desarrolladores.
2) Complete algunos detalles básicos sobre la aplicación (tenga en cuenta el ID DEL CLIENTE que se muestra aquí; lo necesitaremos más adelante).
3) Agregar un usuario bot conectado a la aplicación.
4) Apague el interruptor PUBLIC BOT y observe el token de bot que se muestra (también lo necesitaremos más adelante). Si alguna vez filtra su token de bot, por ejemplo, al publicarlo en una imagen en una publicación del blog de Toptal, es imperativo que lo regenere de inmediato. Cualquier persona en posesión de su token de bot puede controlar la cuenta de su bot y causar problemas potencialmente graves y permanentes para usted y sus usuarios.
5) Agrega el bot a tu gremio de prueba. Para agregar un bot a un gremio, sustituya su ID de cliente (que se muestra anteriormente) en el siguiente URI y navegue hasta él en un navegador.
https://discordapp.com/api/oauth2/authorize?scope=bot&client_id=XXX
Después de hacer clic en autorizar, el bot ahora está en mi gremio de prueba y puedo verlo en la lista de usuarios. Está fuera de línea, pero lo arreglaremos pronto.
Crear el proyecto
Suponiendo que tiene instalado Node.js, cree un proyecto e instale Eris (la biblioteca de bots que usaremos), Express (un marco de aplicación web que usaremos para crear un oyente de webhook) y body-parser (para analizar cuerpos de webhook ).
mkdir premium_bot cd premium_bot npm init npm install eris express body-parser
Conseguir el bot en línea y receptivo
Comencemos con pasos de bebé. Primero, pondremos el bot en línea y nos responderá. Podemos hacer esto en 10-20 líneas de código. Dentro de un nuevo archivo bot.js, necesitamos crear una instancia de Eris Client, pasarle nuestro token de bot (adquirido cuando creamos una aplicación de bot arriba), suscribirnos a algunos eventos en la instancia de Client y decirle que se conecte a Discord . Para fines de demostración, codificaremos nuestro token de bot en el archivo bot.js, pero es una buena práctica crear un archivo de configuración separado y eximirlo del control de código fuente.
(Enlace de código de 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();
Si todo va bien, cuando ejecute este código con su propio token de bot, Connected and ready.
se imprimirá en la consola y verá que su bot se conecta en línea en su servidor de prueba. Puede mencionar 2 a su bot haciendo clic con el botón derecho y seleccionando "Mencionar" o escribiendo su nombre precedido por @. El bot debería responder diciendo "Presente".
2 Mencionar es una forma de llamar la atención de otro usuario, incluso si no está presente. Cuando se mencione a un usuario habitual, se le notificará mediante una notificación en el escritorio, una notificación automática en el móvil y/o un pequeño icono rojo que aparecerá sobre el icono de Discord en la bandeja del sistema. La(s) manera(s) en que se notifica a un usuario depende de su configuración y su estado en línea. Los bots, por otro lado, no reciben ningún tipo de notificación especial cuando se mencionan. Reciben un evento de creación de mensaje regular como lo hacen con cualquier otro mensaje, y pueden verificar las menciones adjuntas al evento para determinar si fueron mencionados.
Registrar Comando de Pago
Ahora que sabemos que podemos poner un bot en línea, deshagámonos de nuestro controlador de eventos Message Create actual y creemos uno nuevo que nos permita informar al bot que recibimos un pago de un usuario.
Para informar al bot del pago, emitiremos un comando que se ve así:
pb!addpayment @user_mention payment_amount
Por ejemplo, pb!addpayment @Me 10.00
para registrar un pago de $10.00 realizado por mí.
El pb! parte se conoce como un prefijo de comando. Es una buena convención elegir un prefijo con el que deben comenzar todos los comandos para su bot. Esto crea una medida de espacio de nombres para los bots y ayuda a evitar la colisión con otros bots. La mayoría de los bots incluyen un comando de ayuda, ¡pero imagina el desastre si tuvieras diez bots en tu gremio y todos respondieran para ayudar ! Usando pb! como prefijo no es una solución infalible, ya que puede haber otros bots que también usen el mismo prefijo. Los bots más populares permiten que su prefijo se configure por gremio para ayudar a prevenir la colisión. Otra opción es usar la mención del propio bot como prefijo, aunque esto hace que la emisión de comandos sea más detallada.
(Enlace de código de 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();
Vamos a intentarlo.
No solo conseguimos que el bot respondiera al comando pb!addpayment
, sino que también creamos un patrón generalizado para manejar los comandos. Podemos agregar más comandos simplemente agregando más controladores al diccionario commandHandlerForCommandName
. Tenemos los ingredientes de un marco de comando simple aquí. El manejo de comandos es una parte tan fundamental de la creación de un bot que muchas personas han escrito marcos de comandos de código abierto que podría usar en lugar de escribir los suyos propios. Los marcos de comandos a menudo le permiten especificar tiempos de reutilización, permisos de usuario requeridos, alias de comandos, descripciones de comandos y ejemplos de uso (para un comando de ayuda generado automáticamente), y más. Eris viene con un marco de comando incorporado.
Hablando de permisos, nuestro bot tiene un pequeño problema de seguridad. Cualquiera puede ejecutar el comando addpayment
. Vamos a restringirlo para que solo el propietario del bot pueda usarlo. Refactorizaremos el diccionario commandHandlerForCommandName
y haremos que contenga objetos JavaScript como sus valores. Esos objetos contendrán una propiedad de execute
con un controlador de comandos y una propiedad botOwnerOnly
con un valor booleano. También codificaremos nuestra ID de usuario en la sección de constantes del bot para que sepa quién es su propietario. Puede encontrar su ID de usuario habilitando el Modo desarrollador en su configuración de Discord, luego haciendo clic derecho en su nombre de usuario y seleccionando Copiar ID.

(Enlace de código de 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();
Ahora el bot se negará airadamente a ejecutar el comando addpayment
si alguien que no sea el propietario del bot intenta ejecutarlo.
A continuación, hagamos que el bot asigne un rol de Premium Member
a cualquiera que done diez dólares o más. En la parte superior del archivo bot.js:
(Enlace de código de 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), ]); }, };
Ahora puedo intentar decir pb!addpayment @Me 10.00
y el bot debería asignarme el rol de Premium Member
.
Vaya, aparece un error de Permisos faltantes en la consola.
DiscordRESTError: DiscordRESTError [50013]: Missing Permissions index.js:85 code:50013
El bot no tiene el permiso Administrar roles en el gremio de prueba, por lo que no puede crear ni asignar roles. Podríamos darle al bot el privilegio de Administrador y nunca más tendríamos este tipo de problema, pero como con cualquier sistema, es mejor darle al usuario (o en este caso a un bot) solo los privilegios mínimos que requiere.
Podemos otorgarle al bot el permiso Administrar roles creando un rol en la configuración del servidor, habilitando el permiso Administrar roles para ese rol y asignando el rol al bot.
Ahora, cuando trato de ejecutar el comando nuevamente, el rol se crea y se me asigna y tengo un color de nombre elegante y una posición especial en la lista de miembros.
En el controlador de comandos, tenemos un comentario TODO que sugiere que debemos verificar si hay argumentos no válidos. Ocupémonos de eso ahora.
(Enlace de código de 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), ]); }, };
Aquí está el código completo hasta ahora:
(Enlace de código de 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();
Esto debería darte una buena idea básica de cómo crear un bot de Discord. Ahora vamos a ver cómo integrar el bot con Ko-fi. Si lo desea, puede crear un webhook en su tablero en Ko-fi, asegurarse de que su enrutador esté configurado para reenviar el puerto 80 y enviarse webhooks de prueba en vivo. Pero solo voy a usar Postman para simular solicitudes.
Los webhooks de Ko-fi entregan cargas útiles que se ven así:
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" }
Vamos a crear un nuevo archivo fuente llamado webhook_listener.js y usar Express para escuchar webhooks. Solo tendremos una ruta Express, y esto es para fines de demostración, por lo que no nos preocuparemos demasiado por usar una estructura de directorio idiomática. Pondremos toda la lógica del servidor web en un archivo.
(Enlace de código de 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;
Luego, solicitemos el nuevo archivo en la parte superior de bot.js para que el oyente comience cuando ejecutamos bot.js.
(Enlace de código de GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step6.js)
const eris = require('eris'); const webhookListener = require('./webhook_listener.js');
Después de iniciar el bot, debería ver "Hola" cuando navegue a http://localhost/kofi en su navegador.
Ahora hagamos que WebhookListener
procese los datos del webhook y emita un evento. Y ahora que hemos probado que nuestro navegador puede acceder a la ruta, cambiemos la ruta a una ruta POST, ya que el webhook de Ko-fi será una solicitud POST.
(Enlace de código de 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.
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.
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
Próximos pasos
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:
- 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. - 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. - 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.
- 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). - 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.
Wrapping It Up
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.