Aparência
Conexão em tempo real
A API HTTP serve para agir. Para ficar sabendo do que acontece — mensagens novas, reações, DMs — o bot mantém uma conexão WebSocket aberta com o Go Live.
wss://apigolive.nemtudo.me/wsO ciclo de vida
Bot Go Live
│── abre wss://.../ws ─────────────────▶│
│◀──────────── { type: "welcome" } ─────│
│── { type: "register", token } ───────▶│
│◀────────── { type: "registered" } ────│ ← a partir daqui chegam eventos
│◀──────── { type: "group-message" } ───│
│◀── { type: "group-message-reactions" }│
│◀──────────────────── ping ────────────│ (a cada 25 s)
│── pong ──────────────────────────────▶│ (automático)
│ ... │Toda mensagem é um objeto JSON com um campo type.
Conectando e se registrando
js
import WebSocket from "ws";
const ws = new WebSocket("wss://apigolive.nemtudo.me/ws");
ws.on("open", () => {
ws.send(JSON.stringify({ type: "register", token: process.env.GOLIVE_TOKEN }));
});
ws.on("message", (data) => {
const event = JSON.parse(data.toString());
switch (event.type) {
case "welcome":
break; // o servidor diz oi; nada a fazer
case "registered":
console.log("Pronto para receber eventos");
break;
case "register-error":
console.error("Registro recusado:", event.message);
break;
case "group-message":
console.log(`${event.author.name}: ${event.message.text}`);
break;
}
});A resposta de sucesso:
js
{
type: "registered",
id: "c0nn3ct10n-1d", // id desta CONEXÃO (não é o id da conta)
name: "DJ do Grupo",
account: { username: "musica_bot", flags: [], bot: true },
guestToken: null
}TIP
O id do registered identifica a conexão, não o bot. O id da conta vem de GET /auth/me.
Heartbeat (ping/pong)
O servidor manda um ping de WebSocket a cada 25 segundos. Uma conexão que não responde com pong é derrubada na rodada seguinte.
Você não precisa fazer nada: a biblioteca ws, o WebSocket nativo do Node 22+ e os navegadores respondem o ping sozinhos. Não confunda com mensagens JSON — não existe um { type: "ping" } para responder.
O que vale a pena fazer é o contrário: perceber quando o servidor sumiu. Se o bot passar ~60 s sem ouvir nada (nem ping), a conexão morreu sem avisar — derrube e reconecte:
js
let silenceTimer;
function touch() {
clearTimeout(silenceTimer);
silenceTimer = setTimeout(() => ws.terminate(), 60_000);
}
ws.on("open", touch);
ws.on("ping", touch);
ws.on("message", touch);Reconectando
Conexões caem: a internet oscila, o Go Live é atualizado, o computador dorme. Um bot de verdade reconecta sozinho, esperando um pouco mais a cada tentativa (backoff exponencial) e com um pouco de aleatoriedade (jitter), para não martelar o servidor:
js
let attempt = 0;
function connect() {
const ws = new WebSocket(GATEWAY);
ws.on("open", () => ws.send(JSON.stringify({ type: "register", token: TOKEN })));
ws.on("message", (data) => {
const event = JSON.parse(data.toString());
if (event.type === "registered") attempt = 0; // deu certo: zera a espera
handle(event);
});
ws.on("close", (code) => {
if (code === 4003) return console.error("Banido — não reconecte.");
if (code === 4004) return console.error("Token trocado ou bot excluído — pegue o token novo no portal.");
const delay = Math.min(30_000, 1000 * 2 ** attempt) + Math.random() * 1000;
attempt += 1;
console.log(`Caiu (${code}). Tentando de novo em ${Math.round(delay)} ms`);
setTimeout(connect, delay);
});
ws.on("error", () => {}); // o "close" vem logo depois e cuida de reconectar
}
connect();Não reconecte em loop sem espera
O servidor aceita no máximo 30 aberturas de conexão por minuto por IP. Um bot que reconecta sem pausa estoura esse limite e passa a ser recusado — o que o faz tentar ainda mais rápido. Sempre espere entre tentativas.
Quando não reconectar
| Situação | O que fazer |
|---|---|
Fechou com código 4003 (ou recebeu { type: "banned" }) | A conta ou o IP está banido. Pare. |
Fechou com código 4004 | O token foi trocado no portal, ou o bot foi excluído. Pare e confira o token. |
register-error sem retryable (ex.: "Invalid name.") | O token é inválido. Pare e confira o token. |
register-error com retryable: true | O servidor está iniciando. Mande o register de novo em alguns segundos. |
O que se perde enquanto está desconectado
Os eventos não são guardados para quem estava fora: o que aconteceu durante a queda não chega depois. Se o seu bot precisa de tudo (um bot de logs, por exemplo), ao reconectar leia o histórico das salas com GET /groups/:id/channels/:cid/messages.
Uma instância só
Cada conexão registrada com o token recebe todos os eventos do bot. Se você rodar o bot duas vezes (no seu PC e no servidor, por exemplo), as duas cópias recebem o mesmo !ping e as duas respondem.
Bot respondendo em dobro?
Quase sempre é uma segunda cópia do bot rodando em algum lugar. Pare todas e deixe uma.
Eventos que um bot recebe
| Evento | Quando |
|---|---|
group-message | Mensagem nova numa sala de texto que o bot vê (inclusive as do próprio bot). |
group-message-updated | O autor editou o texto de uma mensagem. |
group-message-deleted | Uma mensagem foi apagada. |
group-message-reactions | As reações de uma mensagem mudaram. |
group-typing | Alguém começou/parou de digitar. |
group-notify | O bot foi mencionado (ou respondido) numa mensagem. |
group-updated | Algo mudou num grupo do bot. |
group-removed | O bot saiu ou foi tirado de um grupo. |
dm | Mensagem direta enviada ou recebida pelo bot. |
social-update | Pedido de amizade, amizade aceita, bloqueio. |
Os formatos completos estão na referência de eventos. O servidor também manda eventos que um bot pode ignorar (anúncios do site, presença, chamadas) — trate só os type que conhece e ignore o resto.
Mandando coisas pelo WebSocket
Para bots em grupos, o WebSocket é praticamente só de ida: além do register, tudo o que o bot faz (mandar mensagem, reagir, digitar) vai pela API HTTP. A exceção são as salas ao vivo, cujo chat é pelo WebSocket.
Limites da conexão: mensagens de até 64 KB, e no máximo 10 register por minuto.
Uma classe completa
Juntando heartbeat, reconexão e os casos de parada — é a que o bot completo usa:
js
// Conexão em tempo real (WebSocket) com o GoLive.
//
// É por aqui que o bot fica sabendo de tudo que acontece: mensagens novas,
// reações, DMs, mudanças nos grupos. O ciclo é:
//
// 1. abrir wss://apigolive.nemtudo.me/ws
// 2. o servidor manda { type: "welcome" }
// 3. o bot manda { type: "register", token: "Bot <token>" }
// 4. o servidor responde { type: "registered", ... } — a partir daqui os
// eventos chegam
//
// O servidor manda um ping de WebSocket a cada 25 segundos; a biblioteca `ws`
// responde sozinha. Se a conexão cair, reconectamos com espera exponencial.
import WebSocket from "ws";
import { EventEmitter } from "node:events";
import { toCredential } from "./rest.js";
export const DEFAULT_GATEWAY_URL = "wss://apigolive.nemtudo.me/ws";
// Códigos de fechamento em que não adianta reconectar: a conexão foi banida,
// ou o token deixou de valer (trocaram o token no portal, ou o bot foi excluído).
const BANNED_CLOSE_CODE = 4003;
const TOKEN_REVOKED_CLOSE_CODE = 4004;
// O servidor pinga a cada 25s. Sem ouvir nada por 60s, a conexão morreu.
const SILENCE_TIMEOUT_MS = 60_000;
const MAX_RECONNECT_DELAY_MS = 30_000;
export class Gateway extends EventEmitter {
#ws = null;
#attempt = 0;
#silenceTimer = null;
#reconnectTimer = null;
#stopped = false;
constructor({ token, url = DEFAULT_GATEWAY_URL }) {
super();
this.credential = toCredential(token);
this.url = url ?? DEFAULT_GATEWAY_URL;
}
connect() {
this.#stopped = false;
clearTimeout(this.#reconnectTimer);
const ws = new WebSocket(this.url);
this.#ws = ws;
ws.on("open", () => {
this.#touch();
this.#register();
});
// A `ws` já responde o ping com um pong; só anotamos que o servidor está vivo.
ws.on("ping", () => this.#touch());
ws.on("message", (data) => {
this.#touch();
let payload;
try {
payload = JSON.parse(data.toString());
} catch {
return;
}
if (payload && typeof payload.type === "string") this.#handle(payload);
});
ws.on("close", (code, reason) => {
if (this.#ws !== ws) return;
clearTimeout(this.#silenceTimer);
this.#ws = null;
this.emit("disconnect", { code, reason: reason.toString() });
if (this.#stopped) return;
if (code === BANNED_CLOSE_CODE) {
this.emit("error", new Error("A conexão foi recusada: esta conta ou IP está banido."));
return;
}
if (code === TOKEN_REVOKED_CLOSE_CODE) {
this.#stopped = true;
this.emit(
"error",
new Error("O token deste bot foi trocado ou o bot foi excluído. Pegue o token novo no portal do desenvolvedor.")
);
return;
}
this.#scheduleReconnect();
});
// Erros de conexão também disparam "close" logo em seguida, que reconecta.
ws.on("error", (err) => this.emit("debug", `erro no WebSocket: ${err.message}`));
}
/** Envia um objeto pelo WebSocket (ignorado se a conexão não estiver aberta). */
send(payload) {
if (this.#ws?.readyState === WebSocket.OPEN) this.#ws.send(JSON.stringify(payload));
}
/** Fecha de vez, sem reconectar. */
close() {
this.#stopped = true;
clearTimeout(this.#reconnectTimer);
clearTimeout(this.#silenceTimer);
this.#ws?.close(1000);
}
#register() {
this.send({ type: "register", token: this.credential });
}
#handle(payload) {
if (payload.type === "registered") {
this.#attempt = 0;
} else if (payload.type === "register-error") {
if (payload.retryable) {
// O servidor está subindo: tenta de novo em instantes.
setTimeout(() => this.#register(), 3000);
} else {
// Token inválido ou revogado — reconectar não resolve.
this.emit(
"error",
new Error(`O Go Live recusou o registro: ${payload.message}. Confira o token do bot.`)
);
this.close();
}
} else if (payload.type === "banned") {
this.#stopped = true;
}
this.emit("dispatch", payload);
}
#touch() {
clearTimeout(this.#silenceTimer);
this.#silenceTimer = setTimeout(() => {
this.emit("debug", "sem sinal do servidor há 60s, reconectando");
this.#ws?.terminate();
}, SILENCE_TIMEOUT_MS);
}
#scheduleReconnect() {
const delay = Math.min(MAX_RECONNECT_DELAY_MS, 1000 * 2 ** this.#attempt) + Math.random() * 1000;
this.#attempt += 1;
this.emit("reconnecting", { attempt: this.#attempt, delay: Math.round(delay) });
this.#reconnectTimer = setTimeout(() => this.connect(), delay);
}
}