Bots
Um bot é uma conta própria do servidor: entra como membro (com a marca BOT), manda, lê, edita e apaga as próprias mensagens, reage, cria e lê tópicos e recebe em tempo real o que acontece. Para só publicar avisos num canal, um webhook é mais simples. Com a lib oficial, um programa seu consegue:
- enviar mensagens com Markdown, embeds (os cartões formatados do Discord) e botões;
- ler o histórico dos canais e dos tópicos;
- criar tópicos, escrever e ler dentro deles;
- reagir, editar e apagar as próprias mensagens;
- receber o que acontece no servidor em tempo real (mensagens, reações, tópicos, cliques em botões, entradas e saídas).
Tudo o que a lib faz também dá para fazer direto pela API HTTP, sem a lib. Este guia mostra os dois jeitos.
Começo rápido
- No SquadCall, abra o menu do servidor e vá em Integrações (só o dono e os administradores veem).
- Clique em Nova integração, dê um nome e copie o token. Ele aparece uma vez só; se perder, gere outro.
- Instale a lib (Node 18 ou mais novo):
npm install nonseqchat-bot- Rode o primeiro bot:
import { NonseqBot } from 'nonseqchat-bot';
const bot = new NonseqBot({ token: process.env.NONSEQ_TOKEN });
bot.on('message.created', async ({ message }) => {
if (message.content === '!ping') await bot.reply(message, 'pong');
});
const { server, channels } = await bot.connect();
console.log(`Conectado em ${server.name}, vendo ${channels.length} canais.`);NONSEQ_TOKEN=nsc_... node bot.mjsPronto: escreva !ping num canal e o bot responde.
Como funciona
Cada integração é um bot do servidor, uma conta própria que entra como membro. Ela aparece na lista de pessoas com a marca BOT.
- Permissões. O bot vê e escreve só onde as permissões deixam, como qualquer membro. Para usar um canal privado, abra Permissões do canal e libere para ele (ele aparece em "Pessoas").
- Token. O token começa com
nsc_. Quem tem o token fala como o bot, então guarde como senha. Em Integrações > Geral dá para gerar outro; o antigo para na hora. - Eventos. Dá para receber os eventos de dois jeitos. O gateway é uma conexão aberta, em tempo real: é o que
bot.connect()faz e funciona até atrás de roteador, sem endereço público. A URL de eventos é um POST assinado para um servidor seu: bom para funções serverless e serviços que já recebem webhooks. - Webhooks de entrada. Uma URL que publica num canal com um simples POST, sem token nem conexão. Boa para alertas, CI e scripts.
O endereço da API é https://squadcall.gg/chat-api/v1. Para um servidor próprio, passe apiUrl ao criar o bot.
Enviar mensagens
// Texto com Markdown
await bot.messages.send(channelId, '**Deploy** concluído em `produção`.');
// Respondendo uma mensagem (no tópico dela, se ela estiver num)
await bot.reply(message, 'Recebido!');
// Com nome e foto só desta mensagem
await bot.messages.send(channelId, { content: 'Build verde', username: 'CI', avatarUrl: 'https://exemplo.com/ci.png' });
// Editar e apagar as do próprio bot
const sent = await bot.messages.send(channelId, 'Processando...');
await bot.messages.edit(channelId, sent.id, 'Feito.');
await bot.messages.delete(channelId, sent.id);Markdown, embeds e botões: veja Formatação.
Botões e interações
Botões ficam embaixo da mensagem, em até 5 linhas de até 5. Os de link só abrem o endereço. Os outros avisam o bot quando alguém clica: chega um evento interaction com o customId do botão e quem clicou.
import { Buttons, EmbedBuilder, row } from 'nonseqchat-bot';
await bot.messages.send(channelId, {
embeds: [new EmbedBuilder().setTitle('Aprovar o deploy?').setColor('yellow')],
components: [
row(Buttons.success('Aprovar', 'deploy:aprovar'), Buttons.danger('Recusar', 'deploy:recusar')),
row(Buttons.link('Ver o PR', 'https://github.com/minha/app/pull/7')),
],
});
bot.on('interaction', async (interaction) => {
if (!interaction.customId.startsWith('deploy:')) return;
const aprovado = interaction.customId === 'deploy:aprovar';
// Troca a mensagem: novo embed e botões desligados
await interaction.update({
embeds: [new EmbedBuilder().setTitle(aprovado ? 'Aprovado' : 'Recusado').setColor(aprovado ? 'green' : 'red')
.setDescription(`por ${interaction.user.displayName}`)],
components: [],
});
await interaction.reply(`${interaction.user.displayName} ${aprovado ? 'aprovou' : 'recusou'} o deploy.`);
});Estilos: primary (azul), secondary (cinza), success (verde), danger (vermelho) e link. Cada customId vai até 100 caracteres e não se repete na mesma mensagem. Buttons.disabled(botao) mostra o botão apagado. As interações chegam pelo gateway e também pela URL de eventos, se interaction.created estiver marcado.
Tópicos
Tópicos são conversas dentro de um canal de texto, como as threads do Discord. Um tópico nasce de uma mensagem ou do zero, e as mensagens dele não aparecem no canal principal.
// Do zero
const topico = await bot.threads.create(channelId, { name: 'Incidente 24/10' });
await bot.threads.send(topico.id, 'Investigando a lentidão da API.');
// A partir de uma mensagem
const aviso = await bot.messages.send(channelId, 'Nova versão no ar, comentem aqui');
const conversa = await bot.threads.create(channelId, { name: 'Feedback da v1.4', messageId: aviso.id });
// Ler o que foi dito no tópico (mais antigas primeiro)
const mensagens = await bot.threads.messages(conversa.id, { limit: 50 });
// Listar, renomear, arquivar e apagar
const ativos = await bot.threads.list(channelId);
const arquivados = await bot.threads.list(channelId, { archived: true });
await bot.threads.update(conversa.id, { name: 'Feedback v1.4 (resolvido)', archived: true });
await bot.threads.delete(conversa.id); // precisa de permissão de moderar o canalEscrever num tópico arquivado desarquiva ele. Quem criou o tópico e a moderação podem renomear e arquivar.
Ler mensagens
// As 50 mais recentes do canal (sem as dos tópicos), mais antigas primeiro
const recentes = await bot.messages.list(channelId, { limit: 50 });
// Páginas anteriores
const antes = await bot.messages.list(channelId, { before: recentes[0].createdAt, limit: 100 });
// Uma mensagem no contexto (as em volta dela)
const contexto = await bot.messages.list(channelId, { around: messageId });
// Quem é o bot, o servidor e os canais que ele vê
const { user, server, channels } = await bot.me();
const membros = await bot.members();Cada mensagem traz author (com isBot), content, embeds, components, reactions, attachments, replyTo, threadId (se está num tópico) e thread (o tópico que nasceu dela, se houver).
Reações
// emoji: o próprio caractere, como os do seletor de reações do app
await bot.messages.react(channelId, messageId, emoji);
await bot.messages.unreact(channelId, messageId, emoji);
bot.on('reaction.added', ({ emoji, userId, messageId }) => { /* ... */ });Eventos em tempo real (gateway)
bot.connect() abre a conexão e reconecta sozinho se cair. Cada evento chama os handlers com (data, evento).
bot.on('ready', ({ server }) => console.log('pronto em', server.name));
bot.on('message.created', ({ message }) => {});
bot.on('message.updated', ({ message }) => {});
bot.on('message.deleted', ({ id, channelId, threadId }) => {});
bot.on('reaction.added', ({ messageId, emoji, userId }) => {});
bot.on('reaction.removed', ({ messageId, emoji, userId }) => {});
bot.on('thread.created', ({ thread }) => {});
bot.on('thread.updated', ({ thread }) => {});
bot.on('thread.deleted', ({ id, channelId }) => {});
bot.on('interaction', (interaction) => {}); // clique num botão do bot
bot.on('member.joined', ({ user }) => {});
bot.on('member.left', ({ userId, removedBy }) => {});
bot.on('raw', (evento) => {}); // todos, antes dos de cima
bot.on('disconnect', (motivo) => {});
bot.on('reconnect', () => {});
bot.on('error', (erro) => console.error(erro));- O bot só recebe eventos dos canais que pode ler.
- Ele não recebe o que ele mesmo causou (as próprias mensagens e reações), então não entra em loop respondendo a si mesmo.
interaction.createdchega só para o dono do botão, compermissions: o que quem clicou pode fazer naquele canal (interaction.can('SEND_MESSAGES')).
Eventos por URL (webhook de saída)
Em Integrações > Eventos, defina a URL e escolha os eventos. Cada evento vira um POST com o mesmo JSON do gateway:
{
"id": "evt_5f2c...",
"type": "message.created",
"createdAt": "2026-10-25T12:00:00.000Z",
"serverId": "...",
"integrationId": "...",
"data": { "message": { "id": "...", "content": "oi", "author": { "displayName": "Ana" } } }
}Cabeçalhos de cada POST:
| Cabeçalho | O que é |
|---|---|
X-Nonseq-Event | o tipo do evento |
X-Nonseq-Delivery | o id da entrega (o mesmo nas novas tentativas) |
X-Nonseq-Signature | t=<segundos unix>,v1=<HMAC-SHA256 em hex de "<t>.<corpo>"> com o segredo da integração |
Responda 2xx em até 10 segundos. Se falhar, o servidor tenta de novo em 5 s, 30 s, 2 min, 10 min e 1 h e depois desiste. Depois de 30 tentativas seguidas falhando, a URL é desligada até você ligar de novo. O histórico das entregas, com o corpo e o erro de cada uma, fica em Integrações > Eventos, onde dá para reenviar e mandar um ping de teste.
Conferir a assinatura
Sempre confira a assinatura antes de confiar no corpo, e use o corpo cru, antes de converter o JSON.
import http from 'node:http';
import { NonseqBot, createEventHandler } from 'nonseqchat-bot';
const bot = new NonseqBot({ token: process.env.NONSEQ_TOKEN }); // para responder pela API
const vistos = new Set();
http.createServer(createEventHandler(process.env.NONSEQ_SECRET, async (evento) => {
if (vistos.has(evento.id)) return; // o mesmo evento pode chegar duas vezes
vistos.add(evento.id);
bot.handle(evento); // usa os mesmos handlers do gateway
})).listen(3000);
bot.on('message.created', async ({ message }) => {
if (message.content.startsWith('!ajuda')) await bot.reply(message, 'Comandos: !ping, !ajuda');
});Com Express, use o corpo cru na rota:
import express from 'express';
import { parseEvent } from 'nonseqchat-bot';
const app = express();
app.post('/eventos', express.raw({ type: 'application/json' }), (req, res) => {
let evento;
try { evento = parseEvent(process.env.NONSEQ_SECRET, req.body, req.get('x-nonseq-signature')); }
catch { return res.sendStatus(401); }
res.sendStatus(200);
// trate o evento aqui
});Sem a lib, em qualquer linguagem: calcule HMAC-SHA256(segredo, t + "." + corpo), compare em tempo constante com v1 e recuse se t tiver mais de 5 minutos de diferença do seu relógio.
API HTTP
Todas as rotas abaixo aceitam Authorization: Bot <token> e respondem JSON. As permissões são as do bot no servidor.
| Método e rota | O que faz |
|---|---|
GET /bot/me | o bot, o servidor e os canais de texto que ele vê |
GET /servers/:id/layout | categorias e canais |
GET /servers/:id/members | membros (com online e user.isBot) |
GET /channels/:id/messages?limit=&before=&around= | mensagens do canal (sem as dos tópicos) |
POST /channels/:id/messages | envia (content, embeds, components, replyToId, threadId, username, avatar_url) |
PATCH /channels/:id/messages/:messageId | edita (content, embeds, components) |
DELETE /channels/:id/messages/:messageId | apaga |
PUT /channels/:id/messages/:messageId/reactions/:emoji | reage |
DELETE /channels/:id/messages/:messageId/reactions/:emoji | tira a reação |
GET /channels/:id/messages/:messageId/reactions/:emoji?limit=25 | quem reagiu com o emoji, na ordem em que reagiram (até 100) |
GET /channels/:id/threads?archived=true | tópicos do canal |
POST /channels/:id/threads | cria tópico (name, messageId opcional) |
GET /threads/:id | um tópico |
PATCH /threads/:id | renomeia ou arquiva (name, archived) |
DELETE /threads/:id | apaga o tópico e as mensagens dele |
GET /threads/:id/messages | mensagens do tópico (mesma paginação) |
POST /threads/:id/messages | escreve no tópico |
GET /servers/:id/search?q= | busca no servidor |
POST /webhooks/:id/:token | webhook de entrada (sem token de bot) |
curl -X POST "https://squadcall.gg/chat-api/v1/channels/CANAL/messages" \
-H "Authorization: Bot nsc_..." \
-H "Content-Type: application/json" \
-d '{"content":"Olá do curl","embeds":[{"title":"Teste","color":5793266}]}'O gateway é um Socket.IO em https://squadcall.gg com path: '/chat-api/socket.io/' e auth: { botToken }. Ele envia ready ao conectar e event a cada evento, com o mesmo JSON da URL de eventos.
Limites e erros
- Os erros vêm como
{ "message": "..." }em português, com o status HTTP. Na lib viramNonseqApiError, comstatusemessage. 401: token inválido ou trocado.403: o bot não tem permissão (libere o canal para ele) ou a rota não é para bots.404: não existe ou o bot não vê.429: rápido demais.- A lib espera e tenta de novo sozinha quando recebe 429, até três vezes.
- Limites: 120 requisições por minuto por endereço de rede; 30 tópicos novos por minuto; 30 mensagens por minuto por URL de webhook.
- Bots não recebem mensagens diretas nem pedidos de amizade.
Segurança
- Guarde o token e o segredo de eventos em variáveis de ambiente, nunca no código nem no git.
- Confira a assinatura de todo evento recebido por URL e ignore eventos repetidos pelo
id. - Dê ao bot só os canais de que ele precisa.
- Se algo vazar, troque: token em Geral, segredo em Eventos, URL em Webhooks.
- Apagar a integração tira o bot do servidor e invalida tudo dela. As mensagens que ele já mandou continuam, como "Integração removida".
Exemplos completos
A pasta examples/ tem programas prontos para rodar:
eco.mjs: comandos!ping,!topicoe uma enquete com botões, pelo gateway.eventos-http.mjs: o mesmo bot recebendo os eventos por URL, conferindo a assinatura.webhook.mjs: um aviso formatado mandado por URL de webhook.