Como fazer um bot do Discord: uma visão geral e um tutorial
Publicados: 2022-03-11Discord é uma plataforma de mensagens em tempo real que se apresenta como um “bate-papo de voz e texto completo para jogadores”. Devido à sua interface elegante, facilidade de uso e recursos extensos, o Discord experimentou um rápido crescimento e está se tornando cada vez mais popular, mesmo entre aqueles com pouco interesse em videogames. Entre maio de 2017 e maio de 2018, sua base de usuários explodiu de 45 milhões de usuários para mais de 130 milhões, com mais que o dobro de usuários diários do Slack.
Um dos recursos mais atraentes do Discord do ponto de vista de um desenvolvedor de chatbot é seu suporte robusto para bots programáveis que ajudam a integrar o Discord com o mundo exterior e fornecem aos usuários uma experiência mais envolvente. Os bots são onipresentes no Discord e fornecem uma ampla gama de serviços, incluindo assistência de moderação, jogos, música, pesquisas na Internet, processamento de pagamentos e muito mais.
Neste tutorial de bot do Discord, começaremos discutindo a interface do usuário do Discord e suas APIs REST e WebSocket para bots antes de passar para um tutorial onde escreveremos um bot Discord simples em JavaScript. Por fim, ouviremos o desenvolvedor, por certas métricas, do bot mais popular do Discord e suas experiências no desenvolvimento e manutenção de sua significativa infraestrutura e base de código.
Interface do usuário do Discord
Antes de discutirmos os detalhes técnicos, é importante entender como um usuário interage com o Discord e como o Discord se apresenta aos usuários. A maneira como ele se apresenta aos bots é conceitualmente semelhante (mas, é claro, não visual). Na verdade, os aplicativos oficiais do Discord são construídos nas mesmas APIs que os bots usam. É tecnicamente possível executar um bot dentro de uma conta de usuário comum com pouca modificação, mas isso é proibido pelos termos de serviço do Discord. Os bots devem ser executados em contas de bot.
Aqui está uma olhada na versão 1 do navegador do aplicativo Discord em execução no Chrome.
1 A interface do usuário do Discord para o aplicativo de desktop é praticamente a mesma do aplicativo da Web, empacotado com o Electron. O aplicativo iOS é construído com React Native. O aplicativo Android é um código Java Android nativo.
Vamos decompô-lo.
1. Lista de servidores
Do lado esquerdo está a lista de servidores dos quais sou membro. Se você estiver familiarizado com o Slack, um servidor é análogo a um workspace do Slack e representa um grupo de usuários que podem interagir entre si em um ou mais canais no servidor. Um servidor é gerenciado por seu criador e/ou qualquer equipe que eles selecionem e escolham delegar responsabilidades. O criador e/ou equipe definem as regras, a estrutura dos canais no servidor e gerenciam os usuários.
No meu caso, o servidor da API do Discord está no topo da minha lista de servidores. É um ótimo lugar para obter ajuda e conversar com outros desenvolvedores. Abaixo disso está um servidor que criei chamado Test . Vamos testar o bot que criamos mais tarde lá. Abaixo disso há um botão para criar um novo servidor. Qualquer um pode criar um servidor com apenas alguns cliques.
Observe que, embora o termo usado na interface do usuário do Discord seja Server , o termo usado na documentação do desenvolvedor e na API é Guild . Assim que passarmos a falar sobre tópicos técnicos, passaremos a falar sobre Guildas . Os dois termos são intercambiáveis.
2. Lista de canais
Logo à direita da lista de servidores está a lista de canais para o servidor que estou visualizando no momento (neste caso, o servidor da API do Discord). Os canais podem ser divididos em um número arbitrário de categorias. No servidor da API do Discord, as categorias incluem INFORMAÇÃO, GERAL e LIBS, conforme mostrado. Cada canal funciona como uma sala de bate-papo onde os usuários podem discutir qualquer tópico ao qual o canal seja dedicado. O canal que estamos visualizando no momento (info) tem um fundo mais claro. Os canais que têm novas mensagens desde a última vez que as vimos têm uma cor de texto branca.
3. Visualização do Canal
Esta é a visualização do canal onde podemos ver sobre o que os usuários estão falando no canal que estamos visualizando no momento. Podemos ver uma mensagem aqui, apenas parcialmente visível. É uma lista de links para servidores de suporte para bibliotecas individuais de bots do Discord. Os administradores do servidor configuraram este canal para que usuários comuns como eu não possam enviar mensagens nele. Os administradores usam este canal como um quadro de avisos para postar algumas informações importantes onde podem ser facilmente vistas e não serão abafadas pelo chat.
4. Lista de usuários
À direita, há uma lista dos usuários atualmente online neste servidor. Os usuários são organizados em diferentes categorias e seus nomes têm cores diferentes. Isso é resultado dos papéis que eles têm. Uma função descreve em qual categoria (se houver) o usuário deve aparecer, qual deve ser a cor do nome e quais permissões ele tem no servidor. Um usuário pode ter mais de uma função (e muitas vezes tem), e há alguma matemática de precedência que determina o que acontece nesse caso. No mínimo, cada usuário tem a função @everyone. Outras funções são criadas e atribuídas pela equipe do servidor.
5. Entrada de texto
Esta é a entrada de texto onde eu poderia digitar e enviar mensagens, se tivesse permissão. Como não tenho permissão para enviar mensagens neste canal, não posso digitar aqui.
6. Usuário
Este é o usuário atual. Eu defino meu nome de usuário para “Eu”, para evitar que eu fique confuso e porque sou péssimo em escolher nomes. Abaixo do meu nome de usuário está um número (#9484) que é meu discriminador. Pode haver muitos outros usuários chamados "Eu", mas eu sou o único "Eu#9484". Também é possível definir um apelido para mim por servidor, para que eu possa ser conhecido por nomes diferentes em servidores diferentes.
Essas são as partes básicas da interface do usuário do Discord, mas também há muito mais. É fácil começar a usar o Discord mesmo sem criar uma conta, então fique à vontade para dar uma olhada. Você pode entrar no Discord visitando a página inicial do Discord, clicando em "abrir o Discord em um navegador", escolhendo um nome de usuário e possivelmente jogando uma ou duas rodadas de "clique nas imagens do ônibus".
A API do Discord
A API Discord consiste em duas partes separadas: as APIs WebSocket e REST. De um modo geral, a API WebSocket é usada para receber eventos do Discord em tempo real, enquanto a API REST é usada para realizar ações dentro do Discord.
A API WebSocket
A API WebSocket é usada para receber eventos do Discord, incluindo criação de mensagens, exclusão de mensagens, eventos de kick/ban de usuário, atualizações de permissão de usuário e muito mais. A comunicação de um bot com a API WebSocket, por outro lado, é mais limitada. Um bot usa a API WebSocket para solicitar uma conexão, identificar-se, pulsar, gerenciar conexões de voz e fazer algumas coisas mais fundamentais. Você pode ler mais detalhes na documentação do gateway do Discord (uma única conexão com a API WebSocket é chamada de gateway). Para realizar outras ações, a API REST é usada.
Os eventos da API WebSocket contêm uma carga útil, incluindo informações que dependem do tipo do evento. Por exemplo, todos os eventos Message Create serão acompanhados por um objeto de usuário representando o autor da mensagem. No entanto, o objeto de usuário sozinho não contém todas as informações que há para saber sobre o usuário. Por exemplo, não há informações incluídas sobre as permissões do usuário. Se você precisar de mais informações, poderá consultar a API REST, mas por motivos explicados mais adiante na próxima seção, geralmente você deve acessar o cache que deveria ter construído a partir de cargas recebidas de eventos anteriores. Há uma série de eventos que entregam payloads relevantes para as permissões de um usuário, incluindo, entre outros, Guild Create , Guild Role Update e Channel Update .
Um bot pode estar presente em no máximo 2.500 guildas por conexão WebSocket. Para permitir que um bot esteja presente em mais guildas, o bot deve implementar o sharding e abrir várias conexões WebSocket separadas para o Discord. Se o seu bot é executado dentro de um único processo em um único nó, isso é apenas uma complexidade adicional para você que pode parecer desnecessária. Mas se o seu bot é muito popular e precisa ter seu back-end distribuído em nós separados, o suporte a fragmentação do Discord torna isso muito mais fácil do que seria de outra forma.
A API REST
A API REST do Discord é usada por bots para realizar a maioria das ações, como enviar mensagens, expulsar/banir usuários e atualizar permissões de usuários (amplamente análogos aos eventos recebidos da API WebSocket). A API REST também pode ser usada para consultar informações; no entanto, os bots dependem principalmente de eventos da API WebSocket e armazenam em cache as informações recebidas dos eventos WebSocket.
Há várias razões para isso. Consultar a API REST para obter informações do usuário toda vez que um evento Message Create é recebido, por exemplo, não é dimensionado devido aos limites de taxa da API REST. Também é redundante na maioria dos casos, pois a API WebSocket fornece as informações necessárias e você deve tê-las em seu cache.
No entanto, existem algumas exceções e, às vezes, você pode precisar de informações que não estão presentes em seu cache. Quando um bot se conecta inicialmente a um gateway WebSocket, um evento Ready e um evento Guild Create por guilda em que o bot está presente nesse fragmento são inicialmente enviados ao bot para que ele possa preencher seu cache com o estado atual. Os eventos Guild Create para guildas densamente povoadas incluem apenas informações sobre usuários online. Se o seu bot precisar obter informações sobre um usuário offline, as informações relevantes podem não estar presentes em seu cache. Nesse caso, faz sentido fazer uma solicitação à API REST. Ou, se você precisar obter frequentemente informações sobre usuários offline, você pode optar por enviar um opcode Request Guild Members para a API WebSocket para solicitar membros offline da guilda.
Outra exceção é se seu aplicativo não estiver conectado à API WebSocket. Por exemplo, se o seu bot tiver um painel da Web no qual os usuários possam fazer login e alterar as configurações do bot em seu servidor. O painel da web pode estar sendo executado em um processo separado sem nenhuma conexão com a API WebSocket e sem cache de dados do Discord. Pode ser necessário fazer apenas algumas solicitações de API REST ocasionalmente. Nesse tipo de cenário, faz sentido contar com a API REST para obter as informações de que você precisa.
Wrappers de API
Embora seja sempre uma boa ideia ter alguma compreensão de todos os níveis de sua pilha de tecnologia, usar o Discord WebSocket e as APIs REST diretamente é demorado, propenso a erros, geralmente desnecessário e, na verdade, perigoso.
O Discord fornece uma lista com curadoria de bibliotecas oficialmente examinadas e avisa que:
O uso de implementações personalizadas ou bibliotecas não compatíveis que abusam da API ou causam limites de taxa excessivos pode resultar em banimento permanente.
As bibliotecas oficialmente examinadas pelo Discord geralmente são maduras, bem documentadas e apresentam cobertura completa da API do Discord. A maioria dos desenvolvedores de bots nunca terá um bom motivo para desenvolver uma implementação personalizada, exceto por curiosidade ou bravura!
Neste momento, as bibliotecas oficialmente examinadas incluem implementações para Crystal, C#, D, Go, Java, JavaScript, Lua, Nim, PHP, Python, Ruby, Rust e Swift. Pode haver duas ou mais bibliotecas diferentes para sua linguagem de escolha. Escolher qual usar pode ser uma decisão difícil. Além de verificar a respectiva documentação, você pode querer se juntar ao servidor não oficial da API Discord e ter uma ideia do tipo de comunidade que está por trás de cada biblioteca.
Como fazer um bot de discórdia
Vamos ao que interessa. Vamos criar um bot do Discord que fica no nosso servidor e escuta os webhooks do Ko-fi. Ko-fi é um serviço que permite que você aceite facilmente doações para sua conta do PayPal. É muito simples configurar webhooks lá, ao contrário do PayPal, onde você precisa ter uma conta comercial, por isso é ótimo para fins de demonstração ou processamento de doações em pequena escala.
Quando um usuário doa US$ 10 ou mais, o bot atribuirá a ele uma função de Premium Member
que altera a cor do nome e o move para o topo da lista de usuários online. Para este projeto, usaremos Node.js e uma biblioteca de API do Discord chamada Eris (link da documentação: https://abal.moe/Eris/). Eris não é a única biblioteca JavaScript. Você pode escolher discord.js. O código que vamos escrever seria muito semelhante de qualquer maneira.
Além disso, o Patreon, outro processador de doações, fornece um bot oficial do Discord e suporta a configuração de funções do Discord como benefícios do colaborador. Vamos implementar algo semelhante, mas é claro mais básico.
O código para cada etapa do tutorial está disponível no GitHub (https://github.com/mistval/premium_bot). Algumas das etapas mostradas nesta postagem omitem o código inalterado por brevidade, portanto, siga os links fornecidos para o GitHub se achar que pode estar faltando algo.
Criando uma conta de bot
Antes de começarmos a escrever código, precisamos de uma conta de bot. Antes de podermos criar uma conta de bot, precisamos de uma conta de usuário. Para criar uma conta de usuário, siga as instruções aqui.
Então, para criar uma conta de bot, nós:
1) Crie um aplicativo no portal do desenvolvedor.
2) Preencha alguns detalhes básicos sobre o aplicativo (observe o ID do CLIENTE mostrado aqui—precisaremos dele mais tarde).
3) Adicione um usuário de bot conectado ao aplicativo.
4) Desligue o botão PUBLIC BOT e observe o token do bot mostrado (precisaremos disso mais tarde também). Se você vazar seu token de bot, por exemplo, publicando-o em uma imagem em um post do Toptal Blog, é imperativo que você o regenere imediatamente. Qualquer pessoa que possua seu token de bot pode controlar a conta do seu bot e causar problemas potencialmente sérios e permanentes para você e seus usuários.
5) Adicione o bot à sua guilda de teste. Para adicionar um bot a uma guilda, substitua seu ID de cliente (mostrado anteriormente) no seguinte URI e navegue até ele em um navegador.
https://discordapp.com/api/oauth2/authorize?scope=bot&client_id=XXX
Após clicar em autorizar, o bot está agora na minha guilda de teste e posso vê-lo na lista de usuários. Está offline, mas vamos corrigir isso em breve.
Criando o projeto
Supondo que você tenha o Node.js instalado, crie um projeto e instale o Eris (a biblioteca de bot que usaremos), Express (uma estrutura de aplicativo da Web que usaremos para criar um ouvinte de webhook) e body-parser (para analisar corpos de webhook ).
mkdir premium_bot cd premium_bot npm init npm install eris express body-parser
Colocando o bot online e responsivo
Vamos começar com passos de bebê. Primeiro, apenas colocaremos o bot online e nos responderá. Podemos fazer isso em 10-20 linhas de código. Dentro de um novo arquivo bot.js, precisamos criar uma instância do cliente Eris, passar nosso token de bot (adquirido quando criamos um aplicativo de bot acima), assinar alguns eventos na instância do cliente e instruí-lo a se conectar ao Discord . Para fins de demonstração, codificaremos nosso token de bot no arquivo bot.js, mas é uma boa prática criar um arquivo de configuração separado e isentá-lo do controle de origem.
(Link do código do 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();
Se tudo der certo, quando você executar este código com seu próprio token de bot, Connected and ready.
será impresso no console e você verá seu bot ficar online em seu servidor de teste. Você pode mencionar 2 seu bot clicando com o botão direito do mouse e selecionando "Mencionar" ou digitando seu nome precedido por @. O bot deve responder dizendo “Presente”.
2 Mencionar é uma forma de chamar a atenção de outro usuário, mesmo que ele não esteja presente. Um usuário comum, quando mencionado, será notificado por notificação na área de trabalho, notificação por push móvel e/ou um pequeno ícone vermelho aparecendo sobre o ícone do Discord na bandeja do sistema. A(s) maneira(s) pela qual um usuário é notificado depende de suas configurações e de seu estado online. Os bots, por outro lado, não recebem nenhum tipo de notificação especial quando são mencionados. Eles recebem um evento de Criação de Mensagem normal como fazem para qualquer outra mensagem e podem verificar as menções anexadas ao evento para determinar se foram mencionadas.
Gravar Comando de Pagamento
Agora que sabemos que podemos colocar um bot online, vamos nos livrar do nosso manipulador de eventos Message Create atual e criar um novo que nos permita informar ao bot que recebemos o pagamento de um usuário.
Para informar o bot do pagamento, emitiremos um comando parecido com este:
pb!addpayment @user_mention payment_amount
Por exemplo, pb!addpayment @Me 10.00
para registrar um pagamento de $10,00 feito por mim.
O pb! parte é referido como um prefixo de comando. É uma boa convenção escolher um prefixo com o qual todos os comandos do seu bot devem começar. Isso cria uma medida de namespace para bots e ajuda a evitar colisões com outros bots. A maioria dos bots inclui um comando de ajuda, mas imagine a bagunça se você tivesse dez bots em sua guilda e todos eles respondessem para ajudar ! Usando pb! como um prefixo não é uma solução infalível, pois pode haver outros bots que também usem o mesmo prefixo. Os bots mais populares permitem que seu prefixo seja configurado por guilda para ajudar a evitar colisões. Outra opção é usar a própria menção do bot como prefixo, embora isso torne a emissão de comandos mais detalhada.
(Link do código do 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 tentar.
Não apenas conseguimos que o bot respondesse ao comando pb!addpayment
, mas criamos um padrão generalizado para lidar com comandos. Podemos adicionar mais comandos apenas adicionando mais manipuladores ao dicionário commandHandlerForCommandName
. Temos os ingredientes de uma estrutura de comando simples aqui. Manipular comandos é uma parte tão fundamental da criação de um bot que muitas pessoas escreveram estruturas de comando de código aberto que você pode usar em vez de escrever as suas próprias. As estruturas de comando geralmente permitem especificar cooldowns, permissões de usuário necessárias, aliases de comando, descrições de comando e exemplos de uso (para um comando de ajuda gerado automaticamente) e muito mais. Eris vem com uma estrutura de comando integrada.
Falando em permissões, nosso bot tem um problema de segurança. Qualquer um pode executar o comando addpayment
. Vamos restringi-lo para que apenas o dono do bot possa usá-lo. Vamos refatorar o dicionário commandHandlerForCommandName
e fazer com que ele contenha objetos JavaScript como seus valores. Esses objetos conterão uma propriedade execute
com um manipulador de comandos e uma propriedade botOwnerOnly
com um valor booleano. Também codificaremos nosso ID de usuário na seção de constantes do bot para que ele saiba quem é seu proprietário. Você pode encontrar seu ID de usuário ativando o Modo de Desenvolvedor nas configurações do Discord, clicando com o botão direito do mouse em seu nome de usuário e selecionando Copiar ID.
(Link do código do 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();
Agora, o bot se recusará a executar o comando addpayment
se alguém que não seja o proprietário do bot tentar executá-lo.

Em seguida, vamos fazer com que o bot atribua uma função de Premium Member
a qualquer pessoa que doar dez dólares ou mais. Na parte superior do arquivo bot.js:
(Link do código do 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), ]); }, };
Agora posso tentar dizer pb!addpayment @Me 10.00
e o bot deve me atribuir a função de Premium Member
.
Ops, um erro de permissões ausentes aparece no console.
DiscordRESTError: DiscordRESTError [50013]: Missing Permissions index.js:85 code:50013
O bot não tem a permissão Gerenciar funções na guilda de teste, portanto, não pode criar ou atribuir funções. Poderíamos dar ao bot o privilégio de administrador e nunca teríamos esse tipo de problema novamente, mas como em qualquer sistema, é melhor dar apenas a um usuário (ou neste caso um bot) os privilégios mínimos que eles exigem
Podemos conceder ao bot a permissão Gerenciar funções criando uma função nas configurações do servidor, habilitando a permissão Gerenciar funções para essa função e atribuindo a função ao bot.
Agora, quando tento executar o comando novamente, a função é criada e atribuída a mim e tenho uma cor de nome sofisticada e uma posição especial na lista de membros.
No manipulador de comandos, temos um comentário TODO sugerindo que precisamos verificar argumentos inválidos. Vamos cuidar disso agora.
(Link do código do 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), ]); }, };
Segue o código completo até agora:
(Link do código do 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();
Isso deve lhe dar uma boa ideia básica de como criar um bot do Discord. Agora vamos ver como integrar o bot com o Ko-fi. Se desejar, você pode criar um webhook em seu painel no Ko-fi, certifique-se de que seu roteador esteja configurado para encaminhar a porta 80 e envie webhooks de teste ao vivo para você mesmo. Mas vou usar o Postman para simular requisições.
Webhooks da Ko-fi entregam cargas que se parecem com isso:
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 criar um novo arquivo de origem chamado webhook_listener.js e usar o Express para ouvir webhooks. Teremos apenas uma rota Express, e isso é para fins de demonstração, portanto, não nos preocuparemos muito em usar uma estrutura de diretório idiomática. Vamos apenas colocar toda a lógica do servidor web em um arquivo.
(Link do código do 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;
Em seguida, vamos exigir o novo arquivo na parte superior do bot.js para que o ouvinte seja iniciado quando executarmos o bot.js.
(Link do código do GitHub: https://github.com/mistval/premium_bot/blob/master/src/bot_step6.js)
const eris = require('eris'); const webhookListener = require('./webhook_listener.js');
Após iniciar o bot, você deverá ver “Hello” ao navegar para http://localhost/kofi em seu navegador.
Agora vamos fazer com que o WebhookListener
processe os dados do webhook e emita um evento. E agora que testamos que nosso navegador pode acessar a rota, vamos alterar a rota para uma rota POST, pois o webhook do Ko-fi será uma requisição POST.
(Link do código do 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 passos
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.