Criando um Bot para Discord em PHP
Discord é um aplicativo de chat muito utilizado pela comunidade gamer, mas não exclusivamente por ela, sendo utilizado para formar comunidades sobre diversos assuntos. Ao criar seu servidor no aplicativo ele permite a criação de diversos canais de conversa(chats) para dividir as discussões por assuntos, também permitindo canais de conversa por áudio.
Como em todo tipo de comunidade, se ela crescer muito pode se tornar complicado para seus administradores controlarem seus membros, que podem desrespeitar as regras de convivência ou simplesmente bagunçar o servidor spamando.
Para auxiliar a tarefa de administrar um servidor do Discord podemos criar um bot, este com a capacidade de mandar mensagens automáticas para os membros do servidor, criar comandos que podem ser executados, ou mesmo analisar as mensagens que usuários estão enviando para verificar se palavras impróprias não estão sendo ditas e, se necessário, banir o usuário do servidor.
Primeiro para criar um o bot , precisamos criar um aplicativo no Discord e obter o token de acesso, para isso devemos acessar https://discord.com/developers/applications, em New Application. Ele irá pedir o nome do aplicativo, no meu caso vou chamar BotPHP(sim muito criativo). Após ser criado você será redirecionado para a tela do aplicativo, onde é possível editar o nome dele, descrição, imagem etc. Nesta tela é importante pegar o CLIENT ID
pois ele é necessário para adicionar o bot ao servidor desejado. Após, vá no menu bot, e na nova tela clique em Add Bot, que será redirecionado para a tela do bot, onde está disponível o token de acesso que iremos precisar. Nesta tela também é possível mudar a imagem do bot que irá aparecer no servidor bem como seu nome.
Nesta tela também deve-se marcar a opção SERVER MEMBERS INTENT, se pretende que nosso bot busque a lista de usuários do servidor já que esta é um ação restrita por padrão.
Com o CLIENT ID podemos adicionar o bot ao nosso servidor através do link https://discordapp.com/oauth2/authorize?=&client_id=<CLIENT_ID>&scope=bot&permissions=8
substituindo seu client id nela. A permissão 8 significa que o bot tem permissão de administrador do servidor, valores de permissões diferentes podem ser vistos na tela do bot, selecionando as ações permitidas para o bot. Após acessar o link selecione qual o servido que deseja adicionar o bot.
Para criar o bot iremos utilizar a biblioteca DiscordPHP, que roda no CLI(prompt de comando) e não em um webserver. Ela é uma biblioteca orientada a eventos, utilizando os componente do ReactPHP. Para instalar utilizamos o composer.
composer require team-reflex/discord-php
Agora criamos o código básico do bot para conectar ao servidor, executando php index.php
, já devemos ver o bot na lista de usuários do servidor.
//index.php
ini_set('memory_limit', '-1');
use Discord\Discord;
include 'vendor/autoload.php';
$discord= new Discord([
'token' => 'XXXXXXXXX',
]);
$discord->on('ready', function (Discord $discord) {
foreach ($discord->guilds as $guild) {
echo "Guild: {$guild->name} (" . $guild->members->count() . " membros)" . PHP_EOL;
echo "Channels: " . PHP_EOL;
foreach ($guild->channels as $channel) {
echo "\t\t{$channel->name}" . PHP_EOL;
}
echo "Membros: " . PHP_EOL;
foreach ($guild->members as $member) {;
echo "\t\t{$member->user->username}" . PHP_EOL;
}
}
});
$discord->run();
No nosso exemplo criamos um objeto cliente do Discord
na linha 9, onde as opções de inicialização são passadas como um array associativo, devemos passar no mínimo o valor do nosso token de acesso como visto na linha 10.
Também é possível passar outros valores entre eles:
intents
: é passado como um array e define quais eventos o bot irá receber. Por padrão todas as intents estão disponíveis. Ex.:[Intents::GUILDS, Intents::GUILD_PRESENCES, Intents::GUILD_MEMBERS, Intents::GUILD_MESSAGES]
loadAllMembers
: é um boleano que indica se os membros devem ser buscados e armazenado na inicialização do bot. Como é um processo demorado por padrão está configurado para false.storeMessages
: é um boleano que indica se as mensagens recebidas devem ser armazenadas. Por padrão false.disabledEvents
: é um array de eventos que estão desabilitados. Por padrão nehnum evento está desabilitado. Ex.:[Event::MESSAGE_CREATE, Event::MESSAGE_DELETE, ]
loop
: é uma instancia de do ReactPHP event loop que pode ser fornecido ao cliente em vez de criar um novo loop. Por padrão um novo loop é criado.logger
: uma instancia do logger que implementa a interfaceLoggerInterface
a ser utilizado. Por padrão é criada uma nova instancia do Monolog logger que exibe na stdout.
O DiscordPHP é orientado a eventos que devem ser registrados através do método on
, sendo passado um identificador do evento e um callable, que será chamado quando este evento ocorrer.
O evento 'ready'
é disparado uma única vez quando o bot se conecta com o Discord, registramos os outros eventos dentro do callable associado a ele, no nosso caso uma função. A função chamada para o evento 'ready'
recebe um objeto Discord
.
Deste objeto $discord
, no nosso exemplo, pegamos o guilds
que é um objeto de repositório contendo objetos Guild
. Um objeto Guild
é uma representação de um servidor do Discord, então ao percorrer os itens deste repositório através de um laço foreach
, acessamos cada um dos servidores que o bot está conectado, podendo assim pegar suas informações como, name
: seu nome, channels
: um repositório contendo objetos do tipo Channel representando um canal de chat do servidor e member
: um repositório de objetos Members representando os usuários que estão no servidor.
Recebendo Mensagens do Discord
Para receber e processar mensagens devemos registrar o evento Event::MESSAGE_CREATE
, que ao ser disparado irá chamar uma callable associada passando um objeto Message da mensagem que foi recebida e o objeto do nosso cliente do bot(Discord).
Para exemplificar vamos criar uma função que sempre que um usuário mandar uma mensagem que contenha as palavras “ajuda” ou “dificuldade” ele manda uma mensagem para o usuário avisando para ele utilizar o channel apropriado de ajuda.
$discord->on('ready', function (Discord $discord) {
echo "BotPHP Online...".PHP_EOL;
$discord->on(Event::MESSAGE_CREATE, function(Message $message, Discord $discord){
if($message->author->id === $discord->id) return;
$content = $message->content;
if( str_contains($content,'ajuda') || str_contains($content,'dificuldade')){
$message->channel->sendMessage("{$message->author} se estiver precisando de alguma ajuda poste no canal apropriado <#826529729539604520>");
}
});
});
Na linha 4 registramos um evento de criação de mensagem no servido através do método on
, passando para ele uma função que recebe a mensagem que foi criada e um objeto cliente do nosso bot. Na linha 6 pegamos o id do autor da mensagem, e comparamos com o id do próprio bot, se for executamos um return
para parar a função. Essa verificação deve ser feita para evitar que o bot processe a própria mensagem, podendo assim criar um loop infinito, onde o bot recebe uma mensagem e a responde, criando assim outra mensagem que é processada e é respondida e assim por diante ao infinito e além.
Na linha 8 pegamos o conteúdo da mensagem e armazenamos em uma variável e em seguida(linha 9) verificamos se ela contém as palavras ‘ajuda’ ou ‘dificuldade’ através da função str_contains
, se sim e enviamos uma mensagem no channel em que a mensagem original enviada, fazemos isso através do objeto channel
da mensagem recebida, chamando o método sendMessage
, que recebe a como argumento a mensagem a ser enviada. Este método também pode receber dois outros parâmetros que não iremos utilizar, um boleano sinalizando se a mensagem é tts(Text-to-Speech) , e um objeto Embed, que é uma mensagem especial com imagens, campos, etc, veremos mais adiante.
Na mensagem que enviamos vamos citar o autor da mensagem original, fazemos isso acessando o atributo author
do objeto Message
e inserirmos na mensagem, o objeto $author
já possui o método __toString()
que converte o objeto para a string apropriada de citação. Também inserimos na mensagem o “link” para o canal de ajuda, fazemos isso pegando o id do canal, e inserimos no formato <#[id_channel]>
.
Da mesma forma podemos criar um bot que responde a comandos, simplesmente analisando o conteúdo da mensagem verificando se o inicio corresponde ao padrão estabelecido se sim enviando o uma mensagem em resposta.
$discord->on(Event::MESSAGE_CREATE, function(Message $message, Discord $discord){
if($message->author->id === $discord->id) return;
if (str_starts_with($message->content, "!social")) {
$message->channel->sendMessage("{$message->author} conheça nossas redes sociais - https://www.twitter.com/xxxxx ");
}
});
Também podemos deletar uma mensagem, simplesmente chamando o método delete()
, e banir o autor chamando o método ban()
que recebe como argumentos o numero de dias para limpar as mensagem e o motivo do ban.
$discord->on(Event::MESSAGE_CREATE, function(Message $message, Discord $discord){
if($message->author->id === $discord->id) return;
$content = $message->content;
if( str_contains($content,'rodrigo bobo')){
$message->delete();
$message->channel->sendMessage("{$message->author} você se comportou mal, sinta a irá do bot!!!");
$message->author->ban(1, "Xingou nosso amado rodrigo :)");
}
});
Enviando mensagens Embed
Embeds são mensagens especiais, onde pode-se inserir borda colorida, imagens, campos, títulos, rodapé. Para criar um embed, criamos um objeto Embed
, e o enviamos ao channel através do método sendEmbed
passando o objeto embed, ou como terceiro parâmetro do método sendMessage
como comentado acima.
$embed = new Embed($discord);
$embed->setTitle('Uma mensagem embed')
->setDescription("O campo descrição!")
->setFooter('Conteudo do rodapé')
->setColor( 0xFF0000 )
->addField([
'name' => "Campo 1:",
'value' => 'Valor campo 1',
'inline' => false,
])
->addField([
'name' => 'Campo 2',
'value' => "Valor Campo 2",
'inline' => false,
])
->setThumbnail('https://www.botecodigital.dev.br/wp-content/themes/boteco_v4/img/logob.png')
->setImage('https://ferramentas.botecodigital.dev.br/qrcode_generator/imagem.php?c=Um%20qr%20code&ec=QR_ECLEVEL_L&t=10');
$message->channel->sendEmbed($embed);
Na linha 1 criamos o objeto Embed
passando um objeto Discord
para seu construtor, depois chamamos o setTitle
para adicionar o titulo, setDescription
para o texto de descrição, setFooter
para a mensagem de rodapé, setColor
passando um inteiro da cor da borda desejada(podemos utilizar o formato hexadecimal para facilitar).
Podemos adicionar campos ao embed através do método addField
que recebe um array associativo, com as chaves name
o rótulo do campo, value
o valor do campo, e inline
se os campos devem ser organizados em linha.
Adicionamos uma imagem de Thumbnail através do método setThumbnail
, passando a URL da imagem desejada. Também adicionamos uma imagem através do método setImage
passando por parâmetro a URL da imagem.
Enviando Imagens
Podemos enviar imagens já estão hospedadas em algum servidor através do embed, mas também podemos enviar imagens locais através do método sendFile
passando o filepath
, o nome do arquivo e mensagem.
$message->channel->sendFile('./imagem.jpg', 'filename.jpg', null, false);
Pegando as Mensagens de um channel
Podemos pegar as mensagens já postadas em um channel através do método getMessageHistory
que recebe um array associativo de opções, onde a chave limit
estipula qual o limite de mensagem será obtido, after
indicando que devem ser retornadas as mensagens posteriores a que foi informada(pode informar tanto o id da mensagem quanto o próprio objeto Message
) e before
, retornando as mensagens anteriores a passada.
O método getMessageHistory
retorna uma promisse, então devemos chamar o método done
passando uma função que será chamada quando as mensagens forem recuperadas, estas serão passadas como argumento em uma Collection
.
$channel = $discord->getChannel(826196156597272596);
$channel->getMessageHistory(['limit' => 10])->done(function (Collection $messages) {
foreach ($messages as $message) {
echo $message->author->username . " => " . $message->content . PHP_EOL;
}
});
No exemplo acima, pegamos um channel do servidor do discord através do método getChannel
que recebe um id de um channel do seu servidor. Com ele chamamos o método getMessagesHistory
passando o filtro para pegar somente as 10 últimas mensagens. Quando as mensagens forem recuperadas e a função passada para o done
for chamada percorremos todas as mensagens pegando o autor e conteúdo.
Pegando reações em mensagens.
Podemos pegar as reações de uma mensagem através do atributo reactions
dela que é um objeto Repositório que pode ser percorrido por um foreach
possibilitando o acesso a cada objeto Reaction, que contém informações como qual emoji foi utilizado.
Vamos a um exemplo simples de contar quantos likes cada membros teve em suas mensagens, isso pode ser utilizado por exemplo para realizar algum tipo de votações.
$channel = $discord->getChannel(826196156597272596);
$channel->getMessageHistory(['limit' => 100])->done(function (Collection $messages) {
$total_curtidas = [];
/** @var Message */
foreach ($messages as $message) {
if( !isset($total_curtidas[$message->author->username])) $total_curtidas[$message->author->username] = 0;
/** @var Reaction */
foreach($message->reactions as $reaction){
$emoji = mb_convert_encoding($reaction->emoji->name, 'UTF-32', 'UTF-8');
$hex = bin2hex($emoji);
if($hex == '0001f44d'){
echo $reaction->emoji->name . ' -> '. $hex.PHP_EOL;
$total_curtidas[$message->author->username]++;
}
}
}
echo "Total de Curtidas: " .PHP_EOL;
foreach ($total_curtidas as $member => $curtidas) {
echo "{$member}: {$curtidas}".PHP_EOL;
}
});
Primeiro pegamos o channel que iremos recuperar as mensagens através do método getChannel
do objeto $discord
(linha 1), que recebe o id do channel.
Depois pegamos as mensagens do channel através do método getMessageHistory
passando para ele o limite de 100 mensagens. Quando as mensagens forem recuperadas será chamado o método fornecido para done
com uma Collection das mensagens.
Na linha 4 criamos o array que irá armazenar o total de curtidas de cada membro. Na linha 7 iniciamos o loop para percorrer todas as mensagens recuperadas. Na linha 9 verificamos se o membro autor da mensagem já está no array de contagem de curtidas, se não adicionamos ele com o total de 0.
Na linha 12 percorremos todas as reactions da mensagem atual através do atributo reactions
, que como já falamos é um repositório de objetos Reaction
. Deste objeto pegamos o objeto emoji
e seu nome, convertendo para binário e logo após hexadecimal para compará-lo com o código do emoji. Pode-se fazer a comparação diretamente com o simbolo do emoji inserido no editor de código, não fiz no exemplo pois o wordpress que estou usando para publicar zoa o emoji dentro do código😥. Bom se for igual soma no total de curtidas.
Na linha 22 percorremos o array de curtidas e exibimos o total com o username do membro associado.
Mensagens periódicas e agendadas
Como o DiscordPHP utilizar o ReactPHP internamente podemos utilizar o seu loop para realizar ações periódicas. Para isso utilizamos o método addPeriodicTimer
que aceita o tempo de intervalo em segundos e uma função a ser chamada em cada intervalo.
$channel = $discord->getChannel(826196156597272596);
$discord->getLoop()->addPeriodicTimer(10, function() use ($channel){
$channel->sendMessage("Uma mensagem repetitiva!!!");
});
Na linha 3 recuperamos o objeto de loop através de getLoop
e em seguida chamamos addPeriodicTimer
passando 10 segunda como tempo de repetição(cuidado para não spammar o chat) e uma função para enviar uma mensagem a cada intervalor.
Também podemos agendar uma mensagem através do addTimer
que executa uma função após um determinado tempo em segundos.
$channel = $discord->getChannel(826196156597272596);
$date = new DateTime('2021-04-05 09:46:00');
$now = new DateTime();
$diff = $date->getTimestamp() - $now->getTimestamp();
if($diff > 0){
$discord->getLoop()->addTimer($diff , function() use ($channel) {
$channel->sendMessage("Mensagem agendada");
});
}
Na linha 2 criamos um objeto um objeto DateTime
com a data em que desejamos que a mensagem seja enviada. Na linha 3 criamos um objeto DateTime
com a data atual em que o código foi executado. Na 5 calculamos a diferença em segundos entre a data que desejamos enviar a mensagem e a atual, ou seja quanto tempo deve-se esperar para enviar a mensagem. Se esta diferença for maior que 0 ( se a data ainda não passou) pegamos o objeto de loop e chamamos o método addTimer
passando esse valor e uma função para executar, no nosso caso o envio de uma mensagem simples.
Bom pessoal, podemos ver que a biblioteca DiscordPHP tem recursos bem interessantes e permite, com um pouco de criatividade, criar interações bem legais em nossos servidores do Discord. Como sempre não deixe de consultar a documentação pois foi deixado muitos outros recursos e eventos de fora pois esta postagem ficaria muito extensa.
Para ver um bot de exemplo beeemmmm rudimentar acesse no github.
Bom era isso T++.