Aparência
Criando comandos
Comandos são mensagens que começam com um prefixo — !ajuda, !dado 20, !expulsar @fulano spam. O Go Live não tem comandos de barra nem botões: o bot lê o texto e decide.
Vamos do mais simples ao sistema completo que o bot completo usa.
Anatomia de um comando
!escolher pizza | sushi
│└──┬───┘ └─────┬─────┘
│ nome argumentos
prefixoNível 1: um switch
Serve para bots com 3 ou 4 comandos:
js
const PREFIX = "!";
async function onMessage(event) {
const { message, author } = event;
if (author.bot || !message.text.startsWith(PREFIX)) return;
const [name, ...args] = message.text.slice(PREFIX.length).trim().split(/\s+/);
switch (name.toLowerCase()) {
case "ping":
return reply(event, "Pong! 🏓");
case "dado": {
const sides = Number(args[0]) || 6;
return reply(event, `🎲 ${1 + Math.floor(Math.random() * sides)}`);
}
case "ola":
return reply(event, `Olá, ${author.name}!`);
}
}(reply é a função de Respondendo mensagens.)
Nível 2: comandos como objetos
Quando cresce, cada comando vira um objeto com nome, descrição e a função que executa. Ganhamos apelidos e uma ajuda automática de brinde:
js
const commands = new Map();
function register(command) {
commands.set(command.name, command);
for (const alias of command.aliases ?? []) commands.set(alias, command);
}
register({
name: "ping",
description: "Responde pong",
run: ({ event }) => reply(event, "Pong! 🏓"),
});
register({
name: "ajuda",
aliases: ["help"],
description: "Lista os comandos",
run: ({ event }) => {
const unique = [...new Set(commands.values())];
return reply(event, unique.map((c) => `!${c.name} — ${c.description}`).join("\n"));
},
});
async function onMessage(event) {
const { message, author } = event;
if (author.bot || !message.text.startsWith(PREFIX)) return;
const body = message.text.slice(PREFIX.length).trim();
const [name, ...args] = body.split(/\s+/);
const command = commands.get(name.toLowerCase());
if (!command) return;
await command.run({ event, args, rest: body.slice(name.length).trim() });
}args é o texto quebrado em palavras; rest é tudo depois do nome, inteiro — melhor quando o argumento tem espaços ("!enquete Qual filme? | Matrix | Duna").
Nível 3: um arquivo por comando
Com dezenas de comandos, cada um vai para um arquivo em src/commands/, e o bot carrega a pasta inteira ao iniciar:
src/
├─ index.js
├─ lib/commands.js
└─ commands/
├─ ping.js
├─ dado.js
├─ ajuda.js
└─ expulsar.jsjs
import { randomInt } from "node:crypto";
export default {
name: "dado",
aliases: ["roll"],
description: "Rola um dado (6 lados, ou quantos você disser).",
usage: "!dado [lados]",
cooldown: 2,
async run({ message, args }) {
const sides = args[0] === undefined ? 6 : Number(args[0]);
if (!Number.isInteger(sides) || sides < 2 || sides > 1000) {
return message.reply("Diga um número de lados entre 2 e 1000. Ex.: !dado 20");
}
return message.reply(`🎲 Deu ${randomInt(1, sides + 1)} (d${sides})`);
},
};js
import { randomInt } from "node:crypto";
export default {
name: "escolher",
aliases: ["choose"],
description: "Escolhe uma entre as opções separadas por |.",
usage: "!escolher pizza | hambúrguer | sushi",
async run({ message, rest }) {
const options = rest.split("|").map((o) => o.trim()).filter(Boolean);
if (options.length < 2) return message.reply("Me dê pelo menos duas opções separadas por |.");
return message.reply(`🤔 Eu escolho: ${options[randomInt(options.length)]}`);
},
};js
export default {
name: "ajuda",
aliases: ["help", "comandos"],
description: "Lista os comandos, ou explica um deles.",
usage: "!ajuda [comando]",
async run({ message, args, handler }) {
const p = handler.prefix;
if (args[0]) {
const command = handler.find(args[0].replace(p, ""));
if (!command) return message.reply(`Não conheço o comando "${args[0]}".`);
const aliases = command.aliases?.length ? `\nTambém: ${command.aliases.map((a) => p + a).join(", ")}` : "";
return message.reply(`${p}${command.name} — ${command.description}\nUso: ${command.usage ?? p + command.name}${aliases}`);
}
const lines = [...handler.commands.values()].map((c) => `• ${p}${c.name} — ${c.description}`);
return message.reply(`📖 Comandos disponíveis:\n${lines.join("\n")}\n\nDigite ${p}ajuda <comando> para detalhes.`);
},
};O carregador:
js
import { readdir } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
async function loadFolder(folderUrl) {
const dir = fileURLToPath(folderUrl);
for (const file of await readdir(dir)) {
if (!file.endsWith(".js")) continue;
const module = await import(pathToFileURL(join(dir, file)).href);
register(module.default);
}
}
await loadFolder(new URL("./commands/", import.meta.url));Para criar um comando novo, basta criar um arquivo — nada mais muda.
Lendo argumentos
Números
js
const amount = Number(args[0]);
if (!Number.isInteger(amount) || amount < 1 || amount > 100) {
return message.reply("Diga um número entre 1 e 100.");
}Sempre valide: args[0] pode não existir, ser "abc" ou "99999999".
Menções
Uma pessoa mencionada chega no texto como <@id>. Para extrair os ids:
js
function mentionedIds(text) {
return [...new Set([...text.matchAll(/<@([A-Za-z0-9:_-]{1,80})>/g)].map((m) => m[1]))];
}
// "!abraçar <@a41c...>"
const [targetId] = mentionedIds(message.text);
if (!targetId) return message.reply("Mencione alguém!");
await message.send(`🤗 <@${message.author.id}> abraçou <@${targetId}>`);Texto entre aspas
Para comandos como !apelido "Maria Clara" "Mari":
js
function parseQuoted(input) {
return [...input.matchAll(/"([^"]*)"|(\S+)/g)].map((m) => m[1] ?? m[2]);
}
parseQuoted('"Maria Clara" "Mari" extra'); // ["Maria Clara", "Mari", "extra"]Opções separadas por |
js
// !escolher pizza | hambúrguer | sushi
const options = rest.split("|").map((o) => o.trim()).filter(Boolean);Cooldown (tempo entre usos)
Evita que alguém dispare o mesmo comando sem parar — e protege o bot dos limites da API:
js
const cooldowns = new Map(); // "comando:usuario" → quando libera
function checkCooldown(command, userId) {
if (!command.cooldown) return 0;
const key = `${command.name}:${userId}`;
const now = Date.now();
const until = cooldowns.get(key) ?? 0;
if (now < until) return Math.ceil((until - now) / 1000);
cooldowns.set(key, now + command.cooldown * 1000);
return 0;
}
const wait = checkCooldown(command, author.id);
if (wait) return message.reply(`⏳ Espere ${wait}s para usar de novo.`);Permissões
Um comando de moderação precisa checar se quem pediu pode fazer aquilo — a API só checa o bot. Declare a permissão no comando e confira antes de executar:
js
import { mention, mentionedIds } from "../golive/structures.js";
import { GoLiveApiError } from "../golive/rest.js";
export default {
name: "expulsar",
aliases: ["kick"],
description: "Expulsa alguém do grupo (a pessoa pode voltar com um convite).",
usage: "!expulsar @pessoa [motivo]",
permission: "kickMembers",
async run({ client, message }) {
const [targetId] = mentionedIds(message.text);
if (!targetId) return message.reply(`Mencione quem deve sair. Uso: ${this.usage}`);
if (targetId === client.user.id) return message.reply("Não vou me expulsar 😅");
try {
await client.rest.post(`/groups/${message.groupId}/members/${targetId}/kick`);
} catch (err) {
// 403: o bot não tem a permissão, ou a pessoa tem cargo igual/acima do dele.
if (err instanceof GoLiveApiError && (err.status === 403 || err.status === 404)) {
return message.reply(`Não consegui: ${err.body?.error ?? "sem permissão"}`);
}
throw err;
}
const reason = message.text.replace(/^\S+\s*/, "").replace(/<@[^>]+>/g, "").trim();
return message.send(`👢 ${mention(targetId)} foi expulso(a).${reason ? ` Motivo: ${reason}` : ""}`);
},
};A função que responde "fulano tem a permissão X?" está em Permissões e cargos.
Tratando erros
Um erro dentro de um comando não pode derrubar o bot. Envolva a execução:
js
try {
await command.run({ client, message, args, rest });
} catch (err) {
console.error(`[comando ${command.name}]`, err);
await message.reply("❌ Algo deu errado ao executar esse comando.").catch(() => {});
}E como garantia final, no index.js:
js
process.on("unhandledRejection", (err) => console.error("Promise rejeitada:", err));Prefixo por menção
Além de !, dá para aceitar @bot comando:
js
function stripPrefix(text) {
if (text.startsWith(PREFIX)) return text.slice(PREFIX.length);
const mention = `<@${me.id}>`;
if (text.startsWith(mention)) return text.slice(mention.length);
return null;
}O sistema completo
Juntando tudo — objetos, pasta, apelidos, cooldown, permissão e erros:
js
// Um sistema de comandos com prefixo ("!ping", "!dado 20"...).
//
// Cada comando é um arquivo em src/commands que exporta um objeto:
//
// export default {
// name: "ping",
// aliases: ["p"],
// description: "Responde pong",
// usage: "!ping",
// cooldown: 3, // segundos entre usos, por pessoa
// permission: "kickMembers", // opcional: permissão exigida de quem usa
// async run({ client, message, args, rest, handler }) { ... },
// };
import { readdir } from "node:fs/promises";
import { join } from "node:path";
import { fileURLToPath, pathToFileURL } from "node:url";
export class CommandHandler {
commands = new Map();
#aliases = new Map();
#cooldowns = new Map();
constructor(client, { prefix = "!" } = {}) {
this.client = client;
this.prefix = prefix;
}
register(command) {
if (!command?.name || typeof command.run !== "function") {
throw new Error("Um comando precisa de `name` e de uma função `run`.");
}
this.commands.set(command.name.toLowerCase(), command);
for (const alias of command.aliases ?? []) this.#aliases.set(alias.toLowerCase(), command.name.toLowerCase());
}
/** Importa todos os arquivos .js de uma pasta como comandos. */
async loadFolder(folderUrl) {
const dir = fileURLToPath(folderUrl);
for (const file of (await readdir(dir)).filter((f) => f.endsWith(".js")).sort()) {
const module = await import(pathToFileURL(join(dir, file)).href);
this.register(module.default);
}
}
find(name) {
const key = name.toLowerCase();
return this.commands.get(key) ?? this.commands.get(this.#aliases.get(key));
}
/** "!dado 20 " → { name: "dado", args: ["20"], rest: "20" }; null se não for comando. */
parse(text) {
if (!text.startsWith(this.prefix)) return null;
const body = text.slice(this.prefix.length).trim();
if (!body) return null;
const [name, ...args] = body.split(/\s+/);
return { name: name.toLowerCase(), args, rest: body.slice(name.length).trim() };
}
async handle(message) {
// Nunca responda a bots — nem a si mesmo. É o que evita dois bots
// conversando entre si para sempre.
if (message.author.bot) return;
const parsed = this.parse(message.text);
if (!parsed) return;
const command = this.find(parsed.name);
if (!command) return;
const key = `${command.name}:${message.author.id}`;
const now = Date.now();
const until = this.#cooldowns.get(key) ?? 0;
if (now < until) {
const seconds = Math.ceil((until - now) / 1000);
await message.reply(`⏳ Calma! Espere ${seconds}s para usar ${this.prefix}${command.name} de novo.`);
return;
}
if (command.permission) {
const allowed = await this.client.hasPermission(message.groupId, message.author.id, command.permission);
if (!allowed) {
await message.reply("🚫 Você não tem permissão para usar este comando.");
return;
}
}
if (command.cooldown) this.#setCooldown(key, now + command.cooldown * 1000);
try {
await command.run({ client: this.client, message, args: parsed.args, rest: parsed.rest, handler: this });
} catch (err) {
console.error(`[comando ${command.name}]`, err);
await message.reply("❌ Algo deu errado ao executar esse comando.").catch(() => {});
}
}
#setCooldown(key, until) {
this.#cooldowns.set(key, until);
// Limpa os vencidos de vez em quando, para o mapa não crescer para sempre.
if (this.#cooldowns.size > 1000) {
const now = Date.now();
for (const [k, t] of this.#cooldowns) if (t <= now) this.#cooldowns.delete(k);
}
}
}E o uso no index.js:
js
import { GoLiveClient } from "./golive/client.js";
import { CommandHandler } from "./lib/commands.js";
const client = new GoLiveClient({ token: process.env.GOLIVE_TOKEN });
const commands = new CommandHandler(client, { prefix: "!" });
await commands.loadFolder(new URL("./commands/", import.meta.url));
client.on("messageCreate", (message) => commands.handle(message));
await client.login();