Skip to content

About

Bot

Resources

Stars

1 star

Watchers

1 watching

Forks

Latest commit

 

History

67 Commits

Folders and files

Repository files navigation

🎁 LiveLootBot

Sistema de sorteios automáticos durante transmissões ao vivo, integrando Discord e Twitch.

O criador de conteúdo cadastra prêmios pelo Discord e inicia um ciclo de sorteios. Durante a transmissão, o bot anuncia os vencedores no chat da Twitch, o espectador confirma com um comando, e o resultado volta para o Discord. Só participa quem vinculou as duas contas — o que garante que o prêmio vá para alguém que realmente está na comunidade.

Projeto de Trabalho de Conclusão de Curso.


Como funciona

  Criador                                                    Espectador
     │                                                            │
     │ /registrar_itens                                           │
     │ /iniciar_sorteio                                           │
     ▼                                                            │
 ┌─────────┐   sorteia item ponderado    ┌──────────────┐         │
 │   Bot   │ ──────────────────────────► │  PostgreSQL  │         │
 │ Discord │   sorteia um vinculado      │  make_raffle │         │
 └────┬────┘                             └──────────────┘         │
      │                                                           │
      │ "@fulano, digite !claim"          ┌──────────┐            │
      └─────────────────────────────────► │  Twitch  │ ◄──────────┘
                                          │   chat   │   !claim
                                          └────┬─────┘
                                               │ webhook (EventSub)
                                               ▼
                                     ┌──────────────────┐
                                     │  API + Redis     │
                                     │  registra claim  │
                                     └──────────────────┘
  1. O criador cadastra os itens com pesos (item:peso) e inicia o ciclo
  2. A cada rodada, um sorteio decide se haverá prêmio; havendo, uma função no PostgreSQL escolhe o item de forma ponderada pelo peso
  3. O vencedor é sorteado entre os espectadores que vincularam Discord e Twitch
  4. O bot o menciona no chat da Twitch e abre uma janela para o resgate
  5. O espectador digita !claim; a Twitch notifica a API por webhook, que registra no Redis
  6. O sorteio confirma o resgate, grava o vencedor e anuncia no Discord

Tecnologias

Camada Tecnologia
Bot Python 3.13 · discord.py
API FastAPI · Uvicorn
Banco PostgreSQL 16 · psycopg2 com connection pool
Cache Redis
Integração Twitch Helix API · EventSub (webhook) · OAuth 2.0
Infra Docker Compose · ngrok

Pré-requisitos

Software

  • Python 3.13 — no 3.14 as dependências fixadas não têm wheel pronta e a instalação falha
  • Docker e Docker Compose
  • Git

Contas e cadastros (todos gratuitos)


Instalação

1. Clonar e preparar o ambiente Python

git clone https://github.com/EduardoColoni/LiveLootBot.git
cd LiveLootBot

python3.13 -m venv .venv
source .venv/bin/activate          # Windows: .venv\Scripts\activate
python -m ensurepip --upgrade
pip install -r requirements.txt

Confirme a versão antes de seguir:

python -V        # precisa ser 3.13.x

2. Criar a aplicação do Discord

  1. Em discord.com/developers/applications, clique em New Application e dê um nome

  2. No menu Bot, clique em Reset Token e guarde o valor — é o DISCORD_TOKEN

  3. Ainda em Bot, desça até Privileged Gateway Intents e ative o MESSAGE CONTENT INTENT

    Sem esse intent o comando !autenticar para de funcionar em silêncio — sem erro e sem log. Os comandos de barra não dependem dele.

  4. Em OAuth2 → URL Generator, marque os escopos bot e applications.commands, e as permissões Send Messages e Embed Links. Abra a URL gerada para convidar o bot ao seu servidor

3. Criar a aplicação da Twitch

  1. Em dev.twitch.tv/console/apps, clique em Register Your Application

  2. Category: Chat Bot · Client Type: Confidential

  3. Em OAuth Redirect URLs, cadastre os três endereços abaixo, trocando pelo seu domínio do ngrok (passo 5):

    https://SEU-DOMINIO.ngrok-free.app/twitch_callback/streamer
    https://SEU-DOMINIO.ngrok-free.app/twitch_callback/viewer
    https://SEU-DOMINIO.ngrok-free.app/twitch_callback/bot
    

    Precisam ser idênticos caractere a caractere, sem barra no final. Qualquer diferença resulta em redirect_mismatch na hora de autorizar.

  4. Guarde o Client ID e gere um Client Secret — são CLIENT_ID e CLIENT_SECRET

4. Criar a conta do bot na Twitch

O bot fala no chat com uma conta própria, separada da sua. Crie uma conta nova na Twitch (ou use uma que já tenha) e descubra o ID numérico dela — é o BOT_PLATFORM_ID.

O ID numérico não é o nome de usuário. Dá para obtê-lo por qualquer consulta à API da Twitch, ou por sites de terceiros que convertem nome em ID.

5. Configurar o ngrok

A Twitch precisa alcançar a API pela internet, tanto para os redirecionamentos do OAuth quanto para entregar os eventos de chat. Em desenvolvimento isso é resolvido por um túnel.

  1. Crie a conta e copie o Authtoken do painel — é o NGROK_TOKEN
  2. Em Domains, reserve um domínio estático (o plano gratuito dá um)
  3. Edite PostDocker/ngrok.yml e troque o hostname pelo seu domínio:
tunnels:
  fastapi:
    proto: http
    addr: host.docker.internal:8000
    hostname: SEU-DOMINIO.ngrok-free.app

6. Criar os dois arquivos de configuração

São dois .env separados, em pastas diferentes. Ambos estão no .gitignore.

src/.env — a aplicação

Há um modelo pronto no repositório, com comentários explicando cada valor:

cp src/.env.example src/.env
# Discord
DISCORD_TOKEN=o_token_do_passo_2

# Twitch
CLIENT_ID=o_client_id_do_passo_3
CLIENT_SECRET=o_client_secret_do_passo_3
TWITCH_URL=https://id.twitch.tv/oauth2
BOT_PLATFORM_ID=o_id_numerico_do_passo_4
REDIRECT_URI_STREAMER=https://SEU-DOMINIO.ngrok-free.app/twitch_callback/streamer
REDIRECT_URI_VIEWER=https://SEU-DOMINIO.ngrok-free.app/twitch_callback/viewer
TWITCH_WEBHOOK_SECRET=um_valor_aleatorio

# Criptografia dos tokens
TOKEN_ENCRYPTION_KEY=uma_chave_fernet

# Banco de dados
DB_HOST=localhost
DB_PORT=5432
DB_USER=dev_user
DB_PASSWORD=escolha_uma_senha
DB_NAME=live_loot_bot_test

# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_DB=0

# URL pública da API
URL_BASE=https://SEU-DOMINIO.ngrok-free.app

REDIRECT_URI_BOT é opcional: se não for definida, é derivada automaticamente de URL_BASE. O mesmo vale para BOT_PLATFORM_ID, que tem um valor padrão no código.

REDIRECT_URI_STREAMER e REDIRECT_URI_VIEWER são obrigatórias: os links de autorização que o bot manda no Discord usam esses endereços, e nenhum endereço fica fixo no código. Ao mudar de domínio (outra máquina, um servidor), troque URL_BASE e os três REDIRECT_URI_*, cadastre os novos endereços no app da Twitch e autentique o bot de novo (etapa 2 do Primeiro uso): a inscrição do chat é recriada no endereço novo.

Os dois valores secretos são gerados por você, não vêm de nenhum painel:

source .venv/bin/activate
python -c "import secrets; print(secrets.token_hex(32))"                                 # TWITCH_WEBHOOK_SECRET
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" # TOKEN_ENCRYPTION_KEY

Guarde uma cópia da TOKEN_ENCRYPTION_KEY. Os tokens no banco só podem ser lidos com ela. Se for perdida ou trocada, é preciso refazer as autenticações do Primeiro uso.

PostDocker/.env — os contêineres

cd PostDocker
cp .env.example .env
# Precisam ser IGUAIS a DB_USER, DB_PASSWORD e DB_NAME do src/.env
POSTGRES_USER=dev_user
POSTGRES_PASSWORD=a_mesma_senha
POSTGRES_DB=live_loot_bot_test

NGROK_TOKEN=o_token_do_passo_5

O Docker Compose só substitui ${VARIÁVEL} a partir do .env que fica ao lado do docker-compose.yml — por isso os dois arquivos. Se os valores não baterem, os contêineres sobem mas a aplicação não conecta no banco.

7. Subir a infraestrutura

cd PostDocker
docker compose up -d
docker compose ps

Os três serviços (postgres, redis, ngrok) devem aparecer como running.

O schema do banco é aplicado automaticamente na primeira criação do volume — o migrations/000_schema_inicial.sql é montado no diretório de inicialização do PostgreSQL. Confirme:

docker compose logs postgres | grep 000_schema_inicial

Deve aparecer running /docker-entrypoint-initdb.d/000_schema_inicial.sql. E para ver o resultado:

docker exec -it postgres psql -U dev_user -d live_loot_bot_test -c "\dt"

Esperado: seis tabelas — app_access_token, authenticated_users, raffle_items, streamer, streamer_platform e twitch_eventsub_subscription.

8. Rodar a aplicação

São dois processos, em terminais separados, a partir da raiz do projeto.

# Terminal 1 — API
source .venv/bin/activate
uvicorn src.api.main_api:app --reload --host 0.0.0.0
# Terminal 2 — Bot do Discord
source .venv/bin/activate
python -m src.bot.bot

O --host 0.0.0.0 é obrigatório. Sem ele o uvicorn escuta apenas em 127.0.0.1 e o contêiner do ngrok não alcança a API.

O bot roda como módulo (-m). python src/bot/bot.py falha com ModuleNotFoundError: No module named 'src'.


Primeiro uso

A ordem importa: cada etapa depende da anterior.

1. Autenticar o criador de conteúdo

No seu servidor do Discord:

/autenticar_plataformas

O bot responde com três blocos. Clique em "Autenticação na Twitch" e autorize logado na sua conta (a do canal).

2. Autenticar a conta do bot

O mesmo comando, mas clicando em "Autenticação do Bot" — e desta vez logado na Twitch com a conta do bot. O jeito mais simples é abrir o link numa janela anônima.

Se você autorizar com a conta errada, o sistema recusa e avisa qual conta foi usada e qual era a esperada. Nada é gravado.

Confirme que os dois foram registrados, e cifrados:

SELECT platform_name, platform_id, left(token::text, 30) FROM streamer_platform;

Devem aparecer twitch e twitch_bot, com o token no formato {"ct": "gAAAAA.... Se aparecer um access_token legível, a aplicação não está rodando a versão com criptografia.

3. Conferir a inscrição de eventos do chat

Não há nada para rodar: a inscrição que traz as mensagens do chat (e o !claim) para a API é criada sozinha no fim da etapa 2. A página da autorização do bot termina com "Bot autenticado e chat conectado!". No log da API aparece [EventSub] Inscrição de chat registrada e, logo em seguida, um POST /twitch/eventsub ... 200: é a Twitch enviando o desafio de verificação, assinado com o TWITCH_WEBHOOK_SECRET.

Se o segredo foi trocado, o banco foi recriado ou o domínio do ngrok mudou, antes disso aparece [EventSub] Inscrição ... foi criada com outro segredo ... Recriando... (ou ... aponta para outro endereço. Recriando...). É o esperado: a Twitch não deixa trocar o segredo nem o endereço de uma inscrição, então a antiga é apagada e criada de novo.

Se a página disser "Bot autenticado, mas não foi possível conectar o chat", o token do bot foi gravado e só a inscrição falhou; o motivo aparece logo abaixo. Resolvido o problema, autentique o bot de novo. Para refazer só a inscrição, sem passar pelo navegador, a rota manual continua disponível (a resposta null significa sucesso):

curl "http://localhost:8000/twitch_callback/event_sub_signature?platform_id=SEU_ID_NA_TWITCH"

O status gravado no banco é o do momento da criação (pending); a confirmação é o POST /twitch/eventsub ... 200 no log.

SELECT subscription_id, status FROM twitch_eventsub_subscription;

4. Autenticar os espectadores

Cada espectador, no Discord:

!autenticar

E autoriza na Twitch. Só quem fizer isso participa dos sorteios.

5. Sortear

/registrar_itens      → ex: skin dourada:80, skin prata:20
/iniciar_sorteio

O peso define a chance relativa de cada item. Sem peso informado, o padrão é 50.


Comandos

Comando Quem usa O que faz
/autenticar_plataformas criador Links de autorização do criador e do bot
!autenticar espectador Vincula a conta da Twitch ao Discord
/registrar_itens criador Cadastra prêmios no formato item:peso
/iniciar_sorteio criador Inicia o ciclo (um por servidor)
/parar criador Interrompe o ciclo em execução

Estrutura do projeto

LiveLootBot/
├── src/
│   ├── api/                    # Backend FastAPI
│   │   ├── main_api.py         # Aplicação e registro das rotas
│   │   ├── routes/             # Callbacks de OAuth, envio de mensagens, overlay
│   │   ├── services/           # Webhook do EventSub e serviço de autenticação
│   │   ├── static/             # CSS e logo das páginas de autorização
│   │   └── templates/          # Página de autorização (pagina.html) e overlay para OBS
│   ├── bot/                    # Bot do Discord
│   │   ├── bot.py              # Ponto de entrada
│   │   ├── commands/           # Comandos de barra e de prefixo
│   │   └── services/           # Laço do sorteio e regras
│   ├── core/config.py          # Configuração lida do .env
│   └── database/               # Repositórios de PostgreSQL e Redis
├── migrations/                 # Schema versionado (ver README de lá)
├── PostDocker/                 # Docker Compose e configuração do ngrok
├── tests/                      # Testes automatizados (pytest)
├── requirements.txt
└── requirements-dev.txt        # requirements.txt + pytest

Endpoints da API

Rota Para quê
GET /twitch_callback/streamer Callback do OAuth do criador
GET /twitch_callback/viewer Callback do OAuth do espectador
GET /twitch_callback/bot Callback do OAuth da conta do bot
GET /twitch_callback/twitch_app_access_token Gera um App Access Token manualmente
GET /twitch_callback/event_sub_signature Refaz à mão a inscrição de eventos do chat (a autenticação do bot já a cria)
POST /twitch/eventsub Recebe as mensagens do chat da Twitch
POST /twitch_chatters/send_message Envia mensagem no chat
GET /overlays/winner/{nome} Overlay do vencedor para o OBS

Banco de dados

Seis tabelas, todas ligadas a streamer com ON DELETE CASCADE:

Tabela Conteúdo
streamer Criador de conteúdo, um por servidor do Discord
streamer_platform Tokens do criador e da conta do bot
authenticated_users Espectadores com Discord e Twitch vinculados
raffle_items Prêmios cadastrados, com peso e vencedor
twitch_eventsub_subscription Inscrições de eventos do chat
app_access_token Token da aplicação, renovado automaticamente

Os tokens (streamer_platform.token e app_access_token.token_data) são gravados cifrados com Fernet (AES-128-CBC + HMAC-SHA256), com a chave TOKEN_ENCRYPTION_KEY do src/.env. No banco aparece só um envelope {"alg": "fernet", "kid": 1, "ct": "gAAAA..."}. A coluna expires_at continua em claro, para a validade ser consultada sem decifrar. Na coluna twitch_eventsub_subscription.webhook_secret fica apenas a impressão digital do segredo (sha256:...), nunca o segredo em si.

Cuidado com o cascade: apagar a linha do streamer remove junto os dois tokens, todos os espectadores vinculados, os itens e as inscrições.

A lógica ponderada do sorteio é uma função PL/pgSQL (make_raffle), executada dentro do banco. O schema completo, incluindo as funções, está em migrations/ — veja o README de lá.


Testes

A suíte em tests/ roda contra o PostgreSQL e o Redis de verdade (os do Docker Compose); só a Twitch é simulada. Com a infraestrutura no ar:

source .venv/bin/activate
pip install -r requirements-dev.txt   # só na primeira vez
python -m pytest -v

Os dados reais não são tocados: os testes criam e apagam um banco próprio (live_loot_bot_pytest) e usam o banco 15 do Redis. A API não precisa estar rodando, os testes sobem a deles.


Solução de problemas

O log da API e do bot usa os prefixos [OAuth], [EventSub], [Chat] e [Sorteio], e não mostra tokens nem dados pessoais (nome, e-mail, perfil da Twitch).

ERR_NGROK_8012 ao abrir a URL pública O tráfego chegou ao agente do ngrok, mas o agente não alcançou a API. A linha de erro na própria página diz qual dos três casos é:

  • connection refused — a API não está no ar, ou subiu sem --host 0.0.0.0 e por isso só escuta em 127.0.0.1, onde o contêiner não a enxerga. Confirme com ss -tlnp | grep 8000: precisa aparecer 0.0.0.0:8000.
  • no such host — falta a entrada extra_hosts no serviço ngrok do compose. No Docker do Linux o nome host.docker.internal não existe por conta própria.
  • connection timed out — firewall do sistema descartando o pacote. Veja o item seguinte.

connection timed out no ERR_NGROK_8012 — firewall do sistema Distribuições que vêm com ufw ou firewalld ativos descartam, por padrão, o tráfego que chega das redes do Docker para a máquina. É só o timeout acima, sem nenhum log na API.

Para confirmar, com a API rodando:

curl -s -o /dev/null -w '%{http_code}\n' http://172.17.0.1:8000/docs

Se este comando responde 200 mas o contêiner continua dando timeout, é o firewall: o tráfego originado na máquina passa pela interface lo, que é liberada, e o do contêiner não.

# ufw (Arch, CachyOS, Ubuntu)
sudo ufw allow from 172.16.0.0/12 to any port 8000 proto tcp

# firewalld (Fedora)
sudo firewall-cmd --permanent --zone=trusted --add-source=172.16.0.0/12
sudo firewall-cmd --reload

Libere pela faixa de IP, não pela interface. O compose cria uma rede própria (app-network), que no host aparece como br-<id> e não como docker0 — uma regra presa à docker0 não casa com nada, e o nome da br- muda a cada docker compose down. A faixa 172.16.0.0/12 é privada e cobre todas as bridges do Docker.

redirect_mismatch ao autorizar na Twitch A URL de redirecionamento não está cadastrada no app da Twitch, ou está diferente. Confira os três endereços do passo 3 — sem barra no final, idênticos caractere a caractere.

Token do bot não está no banco (HTTP 503) ao enviar mensagem Falta a etapa 2 do primeiro uso. Rode /autenticar_plataformas e clique em "Autenticação do Bot", logado com a conta do bot.

subscription already exists (HTTP 409) ao criar a inscrição A inscrição existe na Twitch mas não no banco — acontece ao recriar o banco. O sistema trata isso sozinho: se o banco confirma que ela foi criada com o TWITCH_WEBHOOK_SECRET atual, ela é reaproveitada; senão, é apagada e criada de novo com o segredo atual. O mesmo vale para uma inscrição que aponta para outro endereço (domínio do ngrok trocado): ela é recriada no URL_BASE atual.

!claim não é reconhecido e o log da API mostra Assinatura inválida A inscrição foi criada com um segredo diferente do TWITCH_WEBHOOK_SECRET atual. Autentique de novo a conta do bot (etapa 2 do Primeiro uso): a inscrição é recriada com o segredo certo.

... está gravado sem criptografia. Refaça a autenticação O banco tem um token de antes da criptografia. Refaça a autenticação indicada (ou recomece do zero, abaixo). O App Access Token não precisa disso: é regerado sozinho.

Não foi possível decifrar o token A TOKEN_ENCRYPTION_KEY do src/.env não é a que cifrou os tokens do banco. Volte a chave antiga ou refaça as autenticações.

ModuleNotFoundError: No module named 'src' O comando foi executado de dentro de uma subpasta, ou pelo caminho do arquivo. Rode da raiz do projeto e use python -m src.bot.bot.

!autenticar não responde O intent Message Content está desativado no Discord Developer Portal. Os comandos de barra continuam funcionando nesse caso, o que torna o sintoma confuso.

A instalação das dependências falha compilando psycopg2 O ambiente virtual foi criado com Python 3.14. Recrie com o 3.13.

Recomeçar do zero

cd PostDocker
docker compose down -v      # apaga os volumes — o banco inteiro vai junto
docker compose up -d

Depois é preciso refazer todas as autenticações do Primeiro uso. A TOKEN_ENCRYPTION_KEY e o TWITCH_WEBHOOK_SECRET do src/.env podem continuar os mesmos.


Autor

Eduardo Coloni — desenvolvimento backend, integrações e automação.

About

Bot

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Contributors

Languages