Como construir um chatbot para Twitch
Um dos primeiros projetos que eu desenvolvi fora da faculdade foi um chatbot para a plataforma de livestream Twitch. Comecei por influência da comunidade e, por ser uma pessoa iniciante, desconhecia padrões de projeto, definições de responsabilidades, entre outras coisas. Nesse artigo, vou mostrar como dei meus primeiros passos nessa jornada.
Uma observação importante: esse projeto foi desenvolvido no sistema operacional Windows, então alguns comandos de terminal podem ter uma sintaxe diferente em outros SO.
Pré requisitos
Para esse projeto, é necessário ter o Python e o PIP previamente instalados na máquina e suas variáveis de ambiente configuradas. Em um primeiro momento, a criação de ambiente virtual e instalação de dependências do projeto foram feitas manualmente.
Criando ambiente virtual
Dentro de um diretório específico para o projeto, abrimos um terminal e executamos o primeiro comando “python -m venv venv” para criação do ambiente virtual. Após finalizada a criação, ativa-se o ambiente com o comando “.\venv\Scripts\activate”, o qual todo projeto será executado dentro desse ambiente.
Se o terminal CMD ficar com essa aparência, sabemos que o ambiente está ativo: “(venv) D:\Projetos\bot>”
Preparando o ambiente
Uma dependência que utilizaremos é o “twitchio”. Esta dependência é quem envolve a API da Twitch e o IRC para fazer comunicações. Para isso faremos a instalação através do comando “pip install twitchio”.
Criando o arquivo principal
Criamos um arquivo “index.py” na raiz do projeto, aqui teremos uma versão minimalista do bot.
from twitchio.ext import commands
class Bot (commands.Bot):
def __init__(self):
super().__init__(token='oauth:...',
nick='bug_elseif', prefix='!',
initial_channels=['bug_elseif'])
async def event_ready(self):
print(f'Ready | {self.nick}')
@commands.command(name='teste')
async def teste(self, ctx):
await ctx.send('teste de comando')
bot = Bot()
bot.run()
Vamos entender o que está acontecendo aqui:
Primeiramente importamos commands a biblioteca “twitchio.ext”.
Criamos uma classe chamada Bot, a qual recebe o commands.Bot, que é a superclasse da biblioteca twitchio. Essa classe tem um construtor que chama a superclasse herdada da biblioteca, a qual recebe as seguintes informações:
- token = é uma chave gerada pela twitch exclusivamente para a conta proprietária do chatbot.
- nick = nome da conta proprietária.
- prefix = identificador de comandos, qualquer mensagem digitada no chat com o prefixo definido, será identificado como um comando, geralmente é utilizado o caracter “!”, porém pode definir qualquer símbolo a sua escolha.
- initial_channels = Diferente dos outros atributos que são do tipo String, este recebe uma lista de canais onde o chatbot pode atuar, os canais descritos aqui devem dar permissão para o canal proprietário através do comando de moderação na própria plataforma da Twitch.
Definindo funções async
Os eventos e comandos disparados pelo canal usuário funcionam de forma assíncrona, ou seja, não é necessário aguardar que um comando no início do código seja executado para que outro, mais ao final, execute.
A primeira função async que temos é “event_ready”, ela serve para nos avisar que o chatbot está pronto e ativo em todos os canais.
A segunda função é um comando. Para eles, usamos uma notação chamada decorator. O decorator é uma forma de modificar o comportamento de uma função. Nesse caso, temos a função “command” a qual recebe um nome (name='teste') como parâmetro para diferenciá-la de outras funções command.
No exemplo o “command” recebe o nome de teste e em seguida definimos uma função de modificação do comando.
O parâmetro “ctx” é definido como um contexto, ele é o canal que está utilizando esse comando e para o qual irá responder.
O “await” é um comando que serve para esperar uma corrotina completar antes de finalizar a função. No exemplo estamos aguardando a função “ctx.send()” finalizar para seguir a função teste.
A função “send()” é quem envia a mensagem no chat do canal setado pelo “ctx”, o parâmetro deve ser sempre uma String.
Toda essa segunda função pode ser usada de base para criar novos comandos. Por fim temos uma variável “bot” que recebe a classe Bot e inicia o loop de eventos assíncronos, se conectando com o IRC da Twitch.
Ativando o chatbot
A ativação do chatbot é manual nesse momento. É necessário os seguintes passos. O primeiro é certificar que o ambiente virtual (venv) esteja ativo no terminal, caso negativo, basta ativá-lo com o comando “.\venv\Scripts\activate”. Dentro do ambiente virtual, navegue até a pasta “venv” e execute o arquivo principal através do comando “python index.py”. Desta forma, já é possível utilizá-lo em todos os canais descritos em “initial_channels”.
Complementando comandos
Existem diversas maneiras de criar comandos a partir da estrutura do comando de teste já apresentada, vamos ver algumas delas.
Usando a lógica dentro do próprio comando
Durante as lives eu exercitava a lógica para aprender mais sobre a sintaxe do Python, para isso, utilizava uma lista de exercícios que abordava estrutura sequencial, decisão, repetição, funções, etc.
Nessa lista surgiu um jogo chamado craps. Foram apresentadas algumas regras e eu deveria criar um código condizente com elas. No momento que eu li as regras do jogo, percebi que era possível colocá-lo como um novo comando no chatbot, chegamos no seguinte código:
import random
@commands.command(name='craps')
async def craps(self, ctx):
nJogada = 1
ponto = 0
while True:
d1 = random.randint(1,7)
d2 = random.randint(1,7)
soma = d1+d2
if nJogada == 1 and (soma == 7 or soma == 11):
mensagem = "Parabéns!! | Pontos: 999 FortOne"
break
elif nJogada == 1 and (soma == 2 or soma == 3 or soma == 12):
mensagem = f"Craps - perdeu | Pontos = {ponto} NotLikeThis"
break
elif nJogada == 1 and (4 <= soma <= 6 or 8 <= soma <= 10):
ponto += soma
nJogada += 1
continue
#-----------------------------------------------------
elif nJogada == 2:
if soma == 7:
mensagem = f"Craps - perdeu | Pontos = {ponto} SeemsGood"
break
else:
ponto += soma
nJogada += 1
continue
elif nJogada > 2:
if 4 <= soma <= 6 or 8 <= soma <= 10:
ponto += soma
nJogada += 1
continue
else:
mensagem = f"Craps - perdeu | Pontos = {ponto} LUL"
break
await ctx.send(f'{ctx.author.name}: {mensagem}')
Essa foi a primeira versão do jogo, onde uma pessoa do chat pode ativar o comando “!craps” e o programa gera números aleatórios chegando em um resultado de vitória ou derrota juntamente com a soma dos pontos e devolvendo para a pessoa as informações.
Para melhor entendimento da lógica do jogo, as regras estão descritas no exercício número 10 desta lista.
A estrutura do comando segue o mesmo padrão do comando de teste, porém com a implementação da lógica do jogo dentro da função, nesse caso no “ctx.send()” estamos enviando as variáveis com o nick da pessoa que ativou o comando juntamente com a variável “mensagem” que é definida de acordo com o número de pontos que a pessoa conseguiu, ambas do tipo Strings.
Usando arquivos Python externos
Um dos comandos criados posteriormente, foi “!musica” o qual retorna um trecho de musica aleatório para a pessoa que o ativou. Criamos um arquivo “musica.py” com uma lista de Strings com pequenos trechos de músicas.
musicas = [
"Aonde quer que eu vá, levo você no olhar",
"Foi pouco tempo mas valeu, vivi cada segundo",
...
]
Esse arquivo é importado dentro do principal “index.py”, onde ele será utilizado.
from musica import *
@commands.command(name='musica')
async def musica(self, ctx):
msg = random.choice(a)
await ctx.send(f'/me {ctx.author.name} - {msg}. SingsNote')
Nesse comando, utilizamos a lista “musicas” para retornar um elemento aleatório com o comando “random.choice()” e atribuímos à variável “msg”, a próxima instrução segue o mesmo padrão do “teste”.
Usando arquivos de texto externos
O comando “!first” foi desenvolvido com o papel de fazer a marcação de quem chegava cedo nas lives. Para seu funcionamento foi necessário um arquivo “.txt” e alguns passos para manipulá-lo.
Inicialmente no construtor da classe, criamos uma leitura de arquivo da seguinte maneira:
with open ("first.txt", "r", encoding="utf-8") as file:
self.lista = file.readlines()
self.data = str(date.today())
if not len(self.lista) or self.lista[0] != self.data+"\n":
with open ("first.txt", "w") as file:
file.write(self.data + "\n")
self.lista = [self.data]
O comando “with” nos garante que o recurso que estamos utilizando será finalizado mesmo que ocorra alguma exceção durante a execução do mesmo. Esse recurso é a abertura de um arquivo de texto com o comando “open”, ele possui três parâmetros:
- Primeiro é o nome do arquivo de texto que será manipulado
2. Segundo é o modo, nesse caso estamos usando o modo leitura (r)
3. Terceiro é a codificação no padrão UTF-8.
Ao final, nomeamos o arquivo como “file” para facilitar o uso dentro do código posteriormente. Armazenaremos na variável lista o conteúdo lido do arquivo de texto, onde cada linha é um índice da lista. Na variável data, vamos armazenar uma string com a data atual.
Na sequência temos uma condicional para verificar essas variáveis. Atendendo as condições, atualizamos os valores tanto das variáveis quanto do arquivo de texto, abrindo-o novamente, dessa vez no modo escrita (w) para modificá-lo. Caso contrário, seguiremos para próxima instrução sem alterações.
Vamos seguir para o comando:
@commands.command(name='first')
async def first(self, ctx):
nome = f'{ctx.author.name}\n'
if nome not in self.lista:
self.lista.append(nome)
with open ("first.txt", "a", encoding="utf-8") as file:
file.write(nome)
if len(self.lista)-1 == 1:
await ctx.send(f'{ctx.author.name} Parabéns, chegou cedo Kappa')
else:
await ctx.send(f'{ctx.author.name} Hoje não, você foi o {len(self.lista)-1}º')
return
await ctx.send(f'{ctx.author.name} Você ja está na lista')
Esse comando também usa a mesma estrutura de base, onde retorna uma mensagem para o chat de acordo com as seguintes condições. Se o nome do nome da pessoa que ativou o comando não está na lista, vamos adicioná-lo tanto na variável quanto no arquivo de texto e depois faremos outra verificação para definir qual das mensagens serão enviadas. Nesse comando, faremos a abertura do arquivo de texto de modo append (a) o qual anexa um novo elemento dentro do arquivo.
Considerações finais
Essa foi a primeira versão do chatbot desenvolvida e implementada de maneira básica, mas de muita importância no início dos meus estudos. Ao aprender mais sobre padrões de projetos, passamos por uma refatoração onde utilizamos a ferramenta Poetry, a qual faz a gerência das dependências, separamos a lógica do arquivo principal organizando o código e deixando-o mais limpo, além de outras melhorias e implementações.