Bots no Samba
Este documento explica como os bots funcionam, como criar o seu e como o servidor é configurado para eles. Tudo aqui foi testado contra o servidor real; o exemplo da seção 8 roda como está.
- 1. Visão geral
- 2. Caramelo, o bot padrão
- 3. Criar um bot
- 4. Autenticação
- 5. Colocar o bot num servidor
- 6. API REST
- 7. Gateway (eventos em tempo real)
- 8. Exemplo completo
- 9. Regras, limites e boas práticas
- 10. Para quem cuida do servidor
- 11. O que ainda não existe
1. Visão geral#
Um bot é uma conta do Samba operada por um programa em vez de uma pessoa. Ele aparece na lista de membros e nas mensagens com o selo BOT, e usa a mesma API que o aplicativo usa: a REST para agir (enviar mensagem, reagir...) e o Gateway (WebSocket) para ouvir o que acontece nos servidores onde está.
Existem dois tipos:
| Tipo | Onde roda | Como é criado |
|---|---|---|
| Padrão (Caramelo e Zeca) | Dentro do próprio servidor do Samba | Nasce com o servidor e entra automaticamente em todos os servidores, os novos e os já existentes |
| De desenvolvedor | No computador ou na hospedagem de quem o criou | Pela API POST /api/bots (seção 3) e adicionado a cada servidor pelo dono dele |
O que um bot pode: ler e enviar mensagens nos canais de texto dos servidores onde foi adicionado, editar e apagar as próprias mensagens, reagir, mostrar "digitando…", enviar imagens e receber os eventos desses servidores.
O que um bot não pode: mandar pedido de amizade, abrir ou receber conversas diretas, criar servidores, entrar em servidor por convite, gerenciar outros bots, nem usar a conta como pessoa (não há login por senha nem pelo Google).
2. Caramelo, o bot padrão#
O Caramelo responde perguntas usando a API do Google Gemini. Em qualquer canal de texto de um servidor:
!c Que dia é hoje?
Ele mostra "digitando…" e responde citando a pergunta. Detalhes:
- Memória do canal: ele lê as últimas mensagens do canal e entende continuações. Depois de
!c Quanto é 5+5?, você pode perguntar!c E o dobro disso?. Só conta a conversa com o Caramelo; o resto do canal ele ignora. - Data e hora: ele sabe a data e a hora de Brasília, então responde "que dia é hoje" corretamente.
- Limites: 5 perguntas por minuto por pessoa e 40 por minuto no servidor inteiro (para proteger seus créditos). Perguntas de até 1500 caracteres. Respostas grandes são divididas em até 3 mensagens.
- Só responde a pessoas. Mensagens de outros bots são ignoradas, para dois bots não entrarem em loop.
!csozinho mostra um exemplo de uso. Sem a chave do Gemini configurada, ele avisa que ainda não foi configurado.- Não dá para removê-lo de um servidor (ele é um bot padrão).
Configuração no servidor (seção 10): SAMBACORD_GEMINI_API_KEY e, opcionalmente, SAMBACORD_GEMINI_MODEL.
Zeca, o bot de música#
O Zeca toca o áudio de vídeos do YouTube dentro da chamada de voz de quem pedir. Em qualquer canal de texto do servidor, estando em uma call de voz desse mesmo servidor:
!m https://www.youtube.com/watch?v=dQw4w9WgXcQ
O Zeca entra na sua call (se ainda não estiver nela), anuncia a música no canal e toca. Detalhes:
| Comando | O que faz |
|---|---|
!m <link do YouTube> | Toca a música, ou coloca na fila se já tiver uma tocando (até 20). Vale youtube.com/watch, youtu.be, shorts, music.youtube.com. Link de playlist toca só o vídeo do link |
!m <nome da música> | Procura no YouTube e toca o primeiro resultado (ele diz qual achou). Ex.: !m costa gold 155. Para ter certeza de qual versão toca, use o link |
!m pausar / !m continuar | Pausa e retoma. Esquecer pausado por 10 minutos faz ele sair da call |
!m pular | Pula para a próxima da fila |
!m anterior | Volta para a música anterior. Se a atual já tocou mais de 5 segundos, recomeça ela |
!m parar | Para tudo, limpa a fila e sai da call |
!m fila | Mostra o que está tocando e a fila |
- Só quem está numa call do servidor pode pedir (e só essa pessoa e quem está na mesma call manda pular ou parar).
- Miniplayer: enquanto o Zeca toca na sua call, aparece um player acima de "Conectado" com voltar, pausar e avançar.
Clicar nele (ou no Zeca) abre o player grande com a fila e o botão de parar. O chat avisa quem apertou
(
gus pausou ⏸️). Os botões chamamPOST /api/music/<pause|resume|skip|previous|stop>. - Uma call por vez. O Zeca toca em um canal de voz de cada vez em todo o Samba. Se alguém de outra call pedir enquanto ele toca, ele avisa que está ocupado.
- Sai sozinho: quando a fila acaba (30 s depois) ou quando a última pessoa sai da call (8 s depois).
- Limites: vídeos de até 60 minutos (
SAMBACORD_MUSIC_MAX_MINUTES), sem transmissões ao vivo, 6 pedidos por minuto por pessoa. - Cada pessoa pode ajustar o volume dele (ou silenciá-lo só para si) pelo perfil, como com qualquer participante.
- O som é mono, 48 kHz, o suficiente para música de call, não para ouvir com qualidade de estúdio.
Como funciona por dentro. O servidor participa da chamada como se fosse mais um navegador (mesma negociação WebRTC de
malha completa do aplicativo, com o módulo nativo @roamhq/wrtc). O yt-dlp baixa o áudio, o ffmpeg converte para
PCM e um relógio de 10 ms entrega os quadros ao WebRTC, que codifica em Opus. O código está em server/bots/music/:
youtube.ts (links e yt-dlp), player.ts (download, decodificação e ritmo), bot-voice.ts (a conexão com a call),
zeca.ts (comandos e fila).
Aviso: é um recurso frágil por natureza.
- Termos do YouTube: baixar áudio fora do player do YouTube vai contra os termos de uso deles. Para um servidor de amigos o risco prático é baixo, mas é uma decisão consciente do dono do Samba.
- O YouTube bloqueia servidores. Rodando no Railway (IP de datacenter) o YouTube costuma responder "confirme que você não é um robô". O Zeca avisa isso no chat. A saída são os cookies de uma conta do YouTube — veja o passo a passo abaixo.
- Algumas músicas funcionam e outras não ("Não consegui pegar essa música no YouTube"): costuma ser o YouTube
recusando o download a partir do IP do servidor, e depende do vídeo. O Zeca tenta uma segunda vez com outros
"clientes" do YouTube antes de desistir, e o log do servidor (Railway → Logs) mostra o que o yt-dlp respondeu, em
linhas que começam com
[zeca]. Os cookies (acima) resolvem a maioria dos casos. - O YouTube muda o tempo todo. O
yt-dlpé atualizado sozinho a cada início do servidor em produção. Se mesmo assim parar de funcionar, reinicie o serviço (ele atualiza de novo) ou veja o log do Railway.
Passo a passo dos cookies (conta secundária do YouTube).
- Use uma conta Google só para isso, não a sua principal: o Google pode estranhar uma conta usada de um servidor.
- Abra uma janela anônima/privada, entre em
youtube.comcom essa conta e, na mesma aba, abrahttps://www.youtube.com/robots.txt(só para a aba ficar parada numa página sem vídeo). - Com uma extensão como "Get cookies.txt LOCALLY", exporte os cookies de
youtube.compara um arquivocookies.txt(formato Netscape). Depois feche a janela anônima. Isso importa: se a sessão continuar aberta, o YouTube troca os cookies e os exportados deixam de valer em poucas horas. - Transforme o arquivo em uma linha de texto (base64). No PowerShell:
[Convert]::ToBase64String([IO.File]::ReadAllBytes("cookies.txt")) | Set-Clipboard - No Railway, em Variables, crie
SAMBACORD_YTDLP_COOKIES_B64e cole o texto. O servidor grava o arquivo no volume (youtube-cookies.txt, só ele lê) ao iniciar e o log mostraYouTube cookies loaded. (Sem Railway: ponha ocookies.txtonde quiser e useSAMBACORD_YTDLP_COOKIES=/caminho/cookies.txt.)
Os cookies vencem de tempos em tempos (semanas ou meses). Quando o Zeca voltar a dizer que o YouTube bloqueou, repita os passos com cookies novos. Nunca publique o arquivo nem a variável: quem tem os cookies controla a conta.
Variáveis do servidor (todas opcionais):
| Variável | Padrão | Para quê |
|---|---|---|
SAMBACORD_YTDLP | yt-dlp | Caminho do executável do yt-dlp |
SAMBACORD_FFMPEG | ffmpeg | Caminho do executável do ffmpeg |
SAMBACORD_YTDLP_COOKIES | (nenhum) | Caminho de um arquivo cookies.txt do YouTube (veja acima) |
SAMBACORD_YTDLP_COOKIES_B64 | (nenhum) | O conteúdo do cookies.txt em base64; o servidor grava o arquivo sozinho (o jeito mais fácil no Railway) |
SAMBACORD_YTDLP_JS_RUNTIME | node | Runtime JavaScript que o yt-dlp usa para resolver os desafios do YouTube (vazio = padrão do yt-dlp) |
SAMBACORD_MUSIC_MAX_MINUTES | 60 | Duração máxima de cada música |
A imagem Docker já instala ffmpeg e yt-dlp. Para rodar o servidor na sua máquina, instale os dois e deixe-os no PATH
(ou aponte as variáveis acima). Sem eles o Zeca continua no servidor, mas responde que não consegue tocar.
Perfil dos bots padrão. Ao clicar num bot padrão (lista de membros ou mensagem) o cartão mostra uma descrição e a lista de
comandos: passe o mouse sobre um comando (ou toque nele, no celular) para ver o que ele faz. Esse texto vem de
src/shared/bots.ts; para um bot padrão novo, descreva-o ali.
3. Criar um bot#
Quem pode criar bots é definido por quem administra o servidor do Samba (variável SAMBACORD_BOT_DEVELOPERS,
seção 10). Por padrão, ninguém. Cada pessoa pode ter até 10 bots.
Com a sua conta (o token de sessão vem do login, POST /api/auth/login):
curl -X POST $URL/api/bots \
-H "Authorization: Bearer $TOKEN_DA_SUA_SESSAO" \
-H "Content-Type: application/json" \
-d '{"name": "MeuBot", "public": false}'
Resposta:
{ "id": "1234567890123456", "username": "MeuBot", "public": false, "token": "1234567890123456.AbC...xyz" }
name: de 2 a 32 caracteres, só letras, números,_,.e-(sem espaços; isso impede que alguém se passe pelos bots padrão, que têm espaço no nome). O nome é único em todo o Samba.public:false(padrão) = só você coloca o bot em servidores seus.true= o dono de qualquer servidor pode adicioná-lo.token: aparece uma única vez. O servidor guarda só um hash, então ninguém consegue recuperá-lo depois. Se perder ou vazar, gere outro (abaixo); o antigo deixa de funcionar na hora.
Outras operações de gerenciamento (sempre com a sessão da pessoa dona, não com o token do bot):
| Operação | Chamada |
|---|---|
| Listar meus bots (e se posso criar) | GET /api/bots → { "canCreate": true, "bots": [{ "id", "username", "public" }] } |
| Gerar um novo token | POST /api/bots/{botId}/token → o bot com o campo token |
| Apagar o bot | DELETE /api/bots/{botId}. Remove o bot de todos os servidores e apaga as mensagens dele |
4. Autenticação#
Toda chamada do bot usa o token no cabeçalho Authorization, com o prefixo Bot (não Bearer):
Authorization: Bot 1234567890123456.AbC...xyz
No Gateway, o prefixo também vale: {"op":"identify","d":{"token":"Bot 1234567890123456.AbC...xyz"}}.
Trate o token como uma senha. Quem o tem controla o bot. Guarde-o em variável de ambiente, nunca no código nem no Git. Se vazou, gere outro com
POST /api/bots/{botId}/token.
Um token válido de bot chamando um endpoint só para pessoas recebe 403; um token inválido ou de um bot apagado, 401.
5. Colocar o bot num servidor#
O dono do servidor adiciona o bot (você só pode usar um bot seu ou um bot público):
curl -X POST $URL/api/guilds/$ID_DO_SERVIDOR/bots \
-H "Authorization: Bearer $TOKEN_DO_DONO" -H "Content-Type: application/json" \
-d '{"botId": "1234567890123456"}'
Os membros recebem o evento GUILD_MEMBER_ADD e o bot recebe GUILD_CREATE com o servidor completo (canais e
membros) pelo Gateway. Para remover: DELETE /api/guilds/{guildId}/bots/{botId} (também só o dono; os bots padrão
não podem ser removidos).
Para saber em quais servidores o bot está sem usar o Gateway: GET /api/bot/me →
{ "user": {...}, "guilds": [{ "guild", "channels", "members" }] }.
6. API REST#
Base: a URL do servidor (por exemplo https://sambachat.app). Corpo e resposta em JSON;
erros vêm como { "error": "mensagem" } com o código HTTP correspondente. Os IDs são strings numéricas (snowflakes);
os mais novos têm IDs maiores, então ordenar por ID ordena por data.
Endpoints que bots podem usar:
| Método e caminho | Para quê |
|---|---|
GET /api/bot/me | Quem sou eu e meus servidores (com canais e membros) |
GET /api/channels/{id}/messages?limit=50&before={messageId} | Histórico (1 a 100, do mais antigo ao mais novo) |
POST /api/channels/{id}/messages | Enviar mensagem |
PATCH /api/channels/{id}/messages/{messageId} | Editar a própria mensagem: { "content": "..." } |
DELETE /api/channels/{id}/messages/{messageId} | Apagar a própria mensagem |
PUT /api/channels/{id}/messages/{messageId}/reaction | Reagir: { "emoji": "👍" } (um emoji por pessoa por mensagem) |
DELETE /api/channels/{id}/messages/{messageId}/reaction | Tirar a própria reação |
POST /api/channels/{id}/typing | Mostrar "digitando…" (dura alguns segundos) |
POST /api/channels/{id}/attachments | Enviar uma imagem (corpo binário, cabeçalho X-Filename); use o id devolvido em attachmentIds |
Enviar uma mensagem:
curl -X POST $URL/api/channels/$ID_DO_CANAL/messages \
-H "Authorization: Bot $TOKEN_DO_BOT" -H "Content-Type: application/json" \
-d '{"content": "Olá, pessoal!", "replyTo": "ID_DE_OUTRA_MENSAGEM"}'
Campos do envio: content (até 2000 caracteres), attachmentIds (até 4), replyTo (opcional, mensagem do mesmo canal).
Uma mensagem precisa ter texto ou anexo.
Menções. No texto, uma pessoa é <@idDaPessoa> e um cargo é <@&idDoCargo> (o & diferencia). O servidor só considera
(e só avisa) o que o autor pode mencionar: cargos "mencionáveis" por qualquer um, ou qualquer cargo e @todos/@aqui para
quem tem a permissão Mencionar todos. Na mensagem recebida, mentions lista as pessoas, mentionRoles os cargos e
mentionEveryone indica @todos. O bot só enxerga os canais dos servidores onde está; qualquer outro
canal responde 404.
O formato de cada objeto (User, Message, Channel, GuildSnapshot...) está em
src/shared/protocol.ts, o mesmo arquivo que o servidor e o aplicativo usam.
Contas de bot têm "bot": true no User.
7. Gateway (eventos em tempo real)#
Para reagir ao que acontece (por exemplo, responder quando alguém escreve !ping), o bot mantém uma conexão
WebSocket em wss://SERVIDOR/gateway. É a mesma conexão que o aplicativo usa.
bot servidor
│ ── abre o WebSocket ───────────►│
│ ◄── {"op":"hello","d":{"heartbeatInterval":30000}}
│ ── {"op":"identify","d":{"token":"Bot ..."}} ──►│
│ ◄── {"op":"dispatch","t":"READY","s":1,"d":{...}} (quem sou eu, servidores, canais, membros)
│ ── {"op":"heartbeat","d":{"seq":1}} ──► (a cada heartbeatInterval)
│ ◄── {"op":"heartbeat_ack"}
│ ◄── {"op":"dispatch","t":"MESSAGE_CREATE","s":2,"d":{...mensagem...}}
Regras:
- Depois do
hello, envie oidentifyem até 10 segundos, ou a conexão é fechada. - Envie um
heartbeata cadaheartbeatInterval(30 s). Se o servidor ficar ~60 s sem receber, ele derruba a conexão. O camposeqé osdo último evento que você recebeu (ounull). - Token inválido →
{"op":"invalid_session","d":{"reason":"invalid token"}}e a conexão fecha. Não adianta reconectar com o mesmo token. - Se a conexão cair, reconecte com espera crescente (1 s, 2 s, 4 s... até 30 s) e identifique de novo. Os eventos perdidos durante a queda não são reenviados: leia o histórico pela REST se precisar.
- Frames de no máximo 64 KB, só texto (JSON).
Eventos que um bot recebe (campo t dos frames dispatch; os de pessoas, como amizades e DMs, não chegam):
| Evento | Quando | Dados |
|---|---|---|
READY | Logo após o identify | { user, guilds: [{ guild, channels, members }], presences, voiceStates, ... } |
GUILD_CREATE | O bot foi adicionado a um servidor | O servidor completo |
GUILD_MEMBER_ADD / GUILD_MEMBER_REMOVE | Alguém (ou um bot) entrou/saiu do servidor | { guildId, user } / { guildId, userId } |
CHANNEL_CREATE | Canal novo | O canal |
MESSAGE_CREATE | Mensagem nova | A mensagem completa |
MESSAGE_UPDATE | Mensagem editada | A mensagem completa, já editada |
MESSAGE_DELETE | Mensagem apagada | { id, channelId } |
MESSAGE_REACTIONS | Reações mudaram | { channelId, messageId, reactions } (a lista inteira) |
TYPING_START | Alguém começou a digitar | { channelId, userId, at } |
PRESENCE_UPDATE | Alguém ficou online/offline | { userId, status } |
VOICE_STATE_UPDATE | Alguém entrou/saiu/mudou na voz | Veja protocol.ts |
Os bots recebem os eventos de todos os servidores onde estão; confira o channelId/guildId do evento.
(O bot também recebe as próprias mensagens: ignore as suas.)
8. Exemplo completo#
docs/bots/exemplos/ping-bot.mjs é um bot de ~70 linhas, sem dependências
(usa o fetch e o WebSocket do Node 22+), que responde !ping com "pong" e !eco texto repetindo o texto.
Ele conecta, faz o heartbeat, reconecta sozinho e ignora outros bots.
SAMBACORD_URL=https://sambachat.app \
SAMBACORD_BOT_TOKEN=1234567890123456.AbC...xyz \
node docs/bots/exemplos/ping-bot.mjs
Passo a passo para vê-lo funcionando: crie o bot (seção 3), adicione-o a um servidor seu (seção 5), rode o comando
acima e escreva !ping num canal desse servidor.
O núcleo do bot é a função que trata cada mensagem; todo o resto é infraestrutura reaproveitável:
async function onMessage(message, me) {
if (message.author.bot) return; // nunca responda bots
if (message.content.trim() === '!ping') await reply(message, 'pong 🏓');
}
9. Regras, limites e boas práticas#
| Limite | Valor |
|---|---|
| Mensagens por segundo por bot | 5 (acima disso: 429 slow down) |
| Tamanho da mensagem | 2000 caracteres |
| Anexos por mensagem | 4 (imagens de até 8 MB) |
| Bots por pessoa | 10 |
Boas práticas:
- Não responda a bots (
message.author.bot) e nunca a si mesmo. Dois bots que respondem um ao outro geram um loop. - Use prefixo de comando (
!ping,!c...) em vez de responder a toda mensagem. - Não faça spam. Quem cuida do servidor pode tirar a permissão de criar bots (seção 10), e o dono de cada servidor pode remover o seu bot de lá.
- Mostre
typing(POST .../typing) em respostas que demoram, e responda citando a mensagem (replyTo). - Não guarde dados pessoais além do necessário; mensagens de um servidor são das pessoas dele.
- Permissões por cargo ainda não existem (seção 11): por enquanto o bot pode tudo que a lista da seção 6 permite, nos servidores onde o dono o colocou. Só adicione bots em quem confia.
10. Para quem cuida do servidor#
Variáveis de ambiente (no Railway, em Variables):
| Variável | Padrão | Para quê |
|---|---|---|
SAMBACORD_GEMINI_API_KEY | (vazia) | Chave da API do Gemini (Google AI Studio). Sem ela o Caramelo responde que não foi configurado |
SAMBACORD_GEMINI_MODEL | gemini-3.8-flash | Modelo usado pelo Caramelo. Os nomes mudam com o tempo: se o Google aposentar o modelo, troque aqui sem alterar código |
SAMBACORD_BOT_DEVELOPERS | (vazia = ninguém) | Nomes de usuário que podem criar bots, separados por vírgula (gustavo,lucas), ou * para todos |
Os bots padrão são criados sozinhos na inicialização e colocados em todos os servidores que ainda não os têm.
A migração do banco (colunas users.is_bot, bot_owner, bot_public e a tabela bot_tokens) é automática e
aditiva.
Para o Caramelo, acompanhe o custo no painel do Google AI Studio. O limite por pessoa e por servidor
(seção 2) é o freio contra abuso; se precisar de outro, ele está em server/bots/samba-chat.ts.
Como é por dentro#
server/db.ts: contas de bot, token (guardado como SHA-256; formato<idDoBot>.<segredo>) e bots padrão.server/rest.ts: autenticaçãoBot, a lista de rotas permitidas a bots (bot: true) e as rotas/api/bots.server/gateway.ts:identifycomBot <token>e os bots internos, que recebem eventos direto na memória.server/bots/runtime.ts: o que um bot padrão pode fazer (BotHost) e a inicialização.server/bots/samba-chat.ts: o Caramelo. Um bot padrão novo é um arquivo assim, registrado emstartBuiltinBots.- Testes:
test/bots.test.ts.
11. O que ainda não existe#
- Comandos com
/(registrados pelo bot, com sugestões) e interações com botões. - Permissões e cargos por bot e por canal (etapa 5 do roadmap).
- Conversa direta com bots (por enquanto só nos canais dos servidores).
- Painel "Meus bots" dentro do aplicativo: hoje a criação é pela API.
- Bots de desenvolvedores na voz: o Zeca participa da chamada por dentro do servidor; bots externos ainda não têm API de voz.
- Webhooks (enviar mensagens por uma URL, sem manter um bot conectado).