DiscordLib: bot de Discord
Bot de Discord por REST sin gateway: ajustes, permisos, ejemplos, garantías del núcleo, Interactions por HTTP y límites en serverless.
En esta página
DiscordLib (core-discordlib, versión 1.1.0) es el proveedor discord del núcleo y el SDK ctx.libs.discord para otras extensiones. Cubre tres piezas independientes: el OAuth de usuario (cada usuario conecta su Discord y, con webhook.incoming, elige su canal), el bot del dominio (una fachada estilo discord.js sobre la API REST de Discord, sin gateway) y las Interactions por HTTP (slash commands, botones, modales y autocompletar). El token del bot nunca sale del núcleo. Sigue el patrón de GoogleLib y los demás proveedores de Proveedores OAuth como extensión.
Crear la aplicación y el bot (Developer Portal)#
- En discord.com/developers/applications pulsa New Application. Copia el Application ID y la Public Key (General Information).
- En Bot pulsa Reset Token y copia el token (solo se muestra una vez). No hace falta activar ningún Privileged Gateway Intent: no hay gateway.
- Excepción: para LEER el
contentde mensajes ajenos por REST, Discord exige el intent Message Content en el portal. Sin él,contentllega vacío salvo en mensajes del propio bot, mensajes directos y menciones. - En OAuth2 > URL Generator marca el scope
bot(yapplications.commandssi registras comandos) con los permisos justos (p. ej. Send Messages y Read Message History). Abre la URL e invita el bot al servidor. - En los ajustes de la extensión guarda Bot ID, Bot Token, y servidor por defecto y/o servidores permitidos. Aprueba los permisos de cada extensión consumidora.
- (Opcional, Interactions) Guarda la Public Key y pega la URL de Interactions en el portal; Discord envía un PING firmado y, si responde 200, queda validada.
Application ID General Information -> DISCORD_APPLICATION_ID (Bot ID)
Public Key General Information -> DISCORD_PUBLIC_KEY (64 hex)
Bot token Bot > Reset Token -> DISCORD_BOT_TOKEN (se muestra una sola vez)
Invitación OAuth2 > URL Generator: scope "bot" (+ "applications.commands")
Interactions URL https://<instancia>/api/ext/core-discordlib/interactionsAjustes#
| Clave | Tipo y formato | Notas |
|---|---|---|
DISCORD_APPLICATION_ID | Texto (snowflake) | Bot ID. Rellena {applicationId} de los comandos; nunca lo elige quien llama. |
DISCORD_BOT_TOKEN | Secreto, forma A.B.C (tres segmentos base64url separados por puntos) | Write-only; cifrado en el núcleo y anclado a discord.com. Se valida la FORMA al guardar y en cada uso, no su validez (la documentación oficial no define el formato). Un token revocado devuelve 401 y se informa como "no configurado". |
DISCORD_PUBLIC_KEY | Secreto, 64 hex | Verifica la firma Ed25519 de las Interactions. |
DISCORD_DEFAULT_GUILD_ID | Texto (snowflake) | Servidor usado si no hay lista. |
allowedGuildIds | Lista de snowflakes | El núcleo rechaza cualquier acción en otro servidor. Vacía = solo el servidor por defecto; sin ninguno = nada. |
allowModeration | Booleano (apagado) | Activa las acciones bot-mod. |
DISCORD_CLIENT_ID / DISCORD_CLIENT_SECRET | Texto / secreto | Solo para el OAuth de usuario (redirect https://<instancia>/api/oauth/discord/callback). |
Permisos del consumidor y riesgo#
Una extensión consumidora declara OAUTH_SHARED:discord y los grupos OAUTH_ACCOUNT:discord:<grupo> que necesite. El administrador aprueba cada uno (siempre aprobación explícita para bot-*). Requiere la capacidad oauth.provider.v3 y la clave de dominio de la instancia (ver Requisito: la clave de dominio).
| Grupo | Riesgo | Acciones (`ctx.libs.discord...`) |
|---|---|---|
bot-read | Medio | guilds.list/fetch, channels.listIn/fetch, guilds.threads, roles.list, members.list/fetch/search, messages.list/fetch, reactions.list, webhooks.list (sin token), users.fetch, commands.list. |
bot-write | Alto | messages.send/edit (sin menciones), reactions.add/remove, threads.create/send/join, commands.register. |
bot-mod | Crítico, apagado por defecto | members.kick/ban/unban, roles.add/remove, messages.remove, commands.remove. Requiere además allowModeration. |
Ejemplos de uso#
La fachada valida antes de llamar: snowflakes, limit (mensajes <= 100, miembros <= 200), content <= 2000, <= 10 embeds (6000 caracteres en total) y comandos (^[-_a-z0-9]{1,32}$, descripción 1..100, <= 25 opciones). Las respuestas se filtran a campos conocidos (sin tokens ni datos del bot).
const d = ctx.libs.discord;
const guild = await d.guilds.fetch("123456789012345678");
const [general] = await guild.channels.list();
const sent = await d.channels.send(general.id, {
content: "Nuevo correo de Ana",
embeds: [{ title: "Asunto", description: "Resumen sin cuerpo" }],
});
await d.reactions.add(general.id, sent.id, "👍");
const last = await d.messages.list(general.id, { limit: 50 }); // limit <= 100
const found = await d.members.search(guild.id, { query: "ana", limit: 5 });
const thread = await d.threads.create(general.id, { name: "Seguimiento", messageId: sent.id });
await d.threads.send(thread.id, "Primer mensaje");
await d.commands.register({ name: "estado", description: "Estado del buzón" }); // global
await d.commands.register({ name: "estado", description: "Estado del buzón" }, { guildId: guild.id }); // de un servidorManejo de errores#
Errores de la fachada: DISCORD_INVALID_ID, DISCORD_INVALID_ARGUMENT, DISCORD_CONTENT_TOO_LONG, DISCORD_TOO_MANY_EMBEDS, DISCORD_MENTIONS_NOT_ALLOWED y DISCORD_API_ERROR (con discordCode). Los del núcleo llegan como OAUTH_ERROR con reason: forbidden, rate_limited, not_configured o quota_exceeded.
try {
await ctx.libs.discord.channels.send(channelId, "Hola");
} catch (e) {
// Fachada: DISCORD_INVALID_ID, DISCORD_INVALID_ARGUMENT, DISCORD_CONTENT_TOO_LONG,
// DISCORD_TOO_MANY_EMBEDS, DISCORD_MENTIONS_NOT_ALLOWED, DISCORD_API_ERROR (+ discordCode)
// Núcleo: OAUTH_ERROR con reason: forbidden | rate_limited (retryAfter) | not_configured | quota_exceeded
if (e.code === "OAUTH_ERROR" && e.reason === "rate_limited") return { retryAfter: e.retryAfter };
throw e;
}Garantías de seguridad del núcleo#
Las aplica el núcleo, no la extensión:
- El token nunca sale:
Authorization: Botse inyecta en el núcleo y jamás se devuelve; las clavestokenysecretse eliminan de toda respuesta. - Host fijo
discord.com(API v10). - Ids validados: snowflakes
^[0-9]{17,20}$, sin traversal de rutas. - Servidores permitidos: servidor, canal e hilo deben pertenecer a
allowedGuildIds(los canales se resuelven a su servidor conGET /channels/{id}; los mensajes directos se rechazan). - Sin menciones:
allowed_mentions.parse = []fijo en todo mensaje saliente. - Moderación apagada por defecto (
allowModeration). - Cuotas por hora por acción (200-600/h en mensajes, 60/h en moderación) además de la cuota por minuto.
- Límites de tasa de Discord: se respetan los cubos
X-RateLimit-*; un 429 conretry_after<= 3 s se espera y se reintenta una vez; si es mayor se devuelverate_limitedconretryAfter. - Respuestas filtradas y con tope de tamaño.
- Auditoría sin contenido:
oauth.actionregistra acción, grupo, servidor, estado y duración, nunca el contenido de los mensajes ni los parámetros.
Interactions por HTTP#
Pega https://<instancia>/api/ext/core-discordlib/interactions como Interactions Endpoint URL en el portal. La ruta es POST /interactions con auth: hmac, algorithm: ed25519 y signedPayload: timestamp+body. Usa siempre el origen de tu instancia (el mismo dominio del webmail), nunca el del backend compartido; los ajustes de la extensión muestran la URL ya calculada con un botón para copiarla. Mientras core-discordlib no esté instalada y activa en la instancia, la ruta responde 404 ROUTE_NOT_FOUND; sin Public Key, 503; firma inválida o timestamp viejo, 401; PING firmado, 200 {"type":1}.
- Firma: se verifica Ed25519 sobre timestamp + cuerpo crudo (cabeceras
X-Signature-Ed25519yX-Signature-Timestamp) conDISCORD_PUBLIC_KEY; se rechazan timestamps fuera de +-5 min y firmas repetidas. Sin clave configurada responde 503 (nunca acepta sin verificar). Cuerpo máximo 64 KB. - PING (
type 1): se responde con{ type: 1 }. - Entrega: comandos, componentes, autocompletar y modales llegan al hook
DISCORD_INTERACTIONde las extensiones conOAUTH_SHARED:discordaprobado, con una vista saneada sin el token de la interacción, en modo público (sinctx.servicesnictx.libs) y con 2,5 s de presupuesto (Discord exige responder en 3 s). El primer handler que responde gana. - Respuestas:
{ respond }(type 4),{ update }(type 7),{ choices }(type 8) o{ defer: true }(type 5/6). - Fase diferida: tras
deferel núcleo contesta de inmediato y, en una segunda fase (ctx.phase === "deferred", hasta 20 s, víaafter()de Next), envía elrespondcomo edición del mensaje original endiscord.com(el token de la interacción solo lo usa el núcleo y caduca a los 15 min). Todo mensaje saliente se sanea (sin menciones, <= 2000 caracteres, <= 10 embeds, solo el flagephemeral).
{
"id": "acme-discord-commands",
"version": "1.0.0",
"permissions": ["OAUTH_SHARED:discord", "OAUTH_ACCOUNT:discord:bot-write"],
"requires": {
"clientApi": 12,
"capabilities": ["oauth.broker.v1", "oauth.provider.v3"],
"extensions": { "core-discordlib": "^1.1.0" }
},
"api": { "functions": { "onInteraction": { "handler": "onInteraction" } } },
"hooks": [{ "point": "DISCORD_INTERACTION", "handler": "onInteraction" }]
}// server.js
export async function onInteraction(ctx) {
const { interaction, phase } = ctx; // sin ctx.libs ni ctx.services (modo público)
if (phase === "deferred") return { respond: { content: "Listo (respuesta diferida)" } };
if (interaction.kind === "command" && interaction.command.name === "estado") {
return { respond: { content: "Buzón al día", ephemeral: true } };
}
if (interaction.kind === "component") return { update: { content: "Pulsado" } };
if (interaction.kind === "autocomplete") return { choices: [{ name: "Entrada", value: "inbox" }] };
if (interaction.kind === "command") return { defer: true, ephemeral: true }; // responde en la fase 2 (hasta 20 s)
}Dentro del hook no hay ctx.libs. Lo que necesites consultar a Discord hazlo antes con un handler normal, o devuelve defer y responde en la fase 2.
Qué NO se puede hacer en serverless#
Discord entrega los eventos en vivo (mensajes nuevos, presencia, reacciones, entradas a servidores, voz) por el gateway, una conexión websocket persistente con heartbeat e intents. Una función serverless nace, responde y muere en segundos: no puede mantener esa conexión. Por eso core-discordlib no implementa gateway, voz ni presencia en tiempo real, y ctx.libs.discord no tiene eventos (client.on(...)). Alternativas:
- Interacciones de usuario en tiempo real: Interactions por HTTP (arriba).
- Estado y mensajes: consultas bajo demanda (
messages.listconafter,members.list,guilds.threads) desde un handlerCRONo una acción del usuario (polling acotado). - Eventos continuos (moderación automática de cada mensaje, presencia, voz): necesitan un proceso persistente externo a esta plataforma que llame al bot; no es posible aquí.
Fuentes oficiales#
- Referencia de la API: formato
Authorization: Bot <token>, snowflakes como cadenas,User-Agent. - Límites de tasa: cabeceras
X-RateLimit-*, 429 conretry_afteryglobal. - Interactions: firma Ed25519, PING y respuestas.
- OAuth2: authorize, token, revoke y scopes
identify,guilds,webhook.incoming.
Compatibilidad: la 1.1.0 solo añade campos de manifest bajo oauth.provider.v3; las instancias sin esa capacidad siguen recibiendo la 1.0.0. Los tipos exactos están en TSDocs: referencia del SDK.