Módulo 6: Real-World Integrations — Slack/Discord
Discord Gateway y slash commands
Descripción
Slack y Discord son ambos "chat platforms", pero su arquitectura de bots es fundamentalmente diferente. Slack manda eventos a tu webhook HTTP. Discord espera que tu bot mantenga una conexión WebSocket abierta (el Gateway) y reciba eventos en tiempo real por ahí.
Esta diferencia cambia tu infraestructura: con Slack podés correr el bot serverless (cold start tolerable). Con Discord necesitás un proceso de larga duración que mantenga la conexión. Cambian también los patterns de scaling — Slack scaleás horizontalmente fácil; Discord requiere sharding cuando crecés a muchos servers.
En esta cápsula vas a entender el modelo de Discord y diseñar tu integración. Vas a ver también las Interactions API (slash commands, buttons, modals) que sí son webhook-based y simplifican muchos casos.
Al terminar vas a poder:
- Entender la diferencia Gateway (WebSocket persistente) vs Interactions (HTTP webhooks)
- Decidir cuándo usás Gateway vs cuándo te alcanza con Interactions
- Diseñar la arquitectura del bot considerando conexión persistente vs serverless
- Implementar slash commands y botones vía Interactions API
- Planificar sharding si tu bot crece a muchos servers
El modelo dual: Gateway + Interactions
Discord ofrece dos APIs distintas:
Gateway (WebSocket)
- Para qué: recibir eventos en tiempo real (mensajes en canales, reactions, presence)
- Cómo funciona: tu bot abre WebSocket a
wss://gateway.discord.gg/, autentica con bot token, recibe eventos en streaming - Requisito: proceso de larga duración manteniendo la conexión
- Heartbeat: tu bot manda pings cada ~30-60s, si no, Discord asume desconexión
Interactions API (HTTP)
- Para qué: slash commands, buttons, modals, context menus — todo lo que es "interacción del usuario con tu bot"
- Cómo funciona: Discord hace POST a tu URL cuando un usuario interactúa
- Timeout: 3 segundos para responder (igual que Slack)
- Requisito: endpoint HTTP público
¿Cuándo cada uno?
| Tu bot necesita... | Usás |
|---|---|
| Reaccionar a mensajes regulares en canales | Gateway |
Solo responder a /comandos y botones | Interactions API |
| Detectar nuevos miembros, reactions, presence | Gateway |
| Bot simple de utilidad (commands only) | Interactions API |
Para AI assistants: muchos casos te alcanza con solo Interactions (/ask, /search, botones de feedback). Eso te permite arquitectura serverless. Si querés que el bot reaccione a mensajes normales (sin comando), necesitás Gateway.
Setup de un bot Discord
- Create application
- Bot section: crea bot, obtené el token (
MzU0OTE0...) - OAuth2 → URL Generator: scopes
botyapplications.commands, permisos relevantes - Genera invite URL:
https://discord.com/api/oauth2/authorize?client_id=...&permissions=...&scope=bot+applications.commands
Permisos típicos: Send Messages, Read Message History, Use Slash Commands.
Interactions API: el camino simple
Configuración
En el Developer Portal → Interactions Endpoint URL: tu URL pública (https://tu-api.com/discord/interactions).
Discord manda un PING al configurar, igual que el URL verification de Slack. Tu endpoint debe verificar y responder.
Verificación de signature
Discord firma con Ed25519 (no HMAC como Slack):
from nacl.signing import VerifyKey
from nacl.exceptions import BadSignatureError
import os
DISCORD_PUBLIC_KEY = os.environ["DISCORD_PUBLIC_KEY"]
verify_key = VerifyKey(bytes.fromhex(DISCORD_PUBLIC_KEY))
def verify_discord_signature(body: bytes, signature: str, timestamp: str) -> bool:
try:
verify_key.verify(
f"{timestamp}{body.decode()}".encode(),
bytes.fromhex(signature),
)
return True
except BadSignatureError:
return False
Requiere pip install pynacl.
El endpoint
from fastapi import FastAPI, Request, BackgroundTasks
from fastapi.responses import JSONResponse
import json
app = FastAPI()
# Tipos de interacción
INTERACTION_PING = 1
INTERACTION_APPLICATION_COMMAND = 2
INTERACTION_MESSAGE_COMPONENT = 3
INTERACTION_MODAL_SUBMIT = 5
# Tipos de respuesta
RESPONSE_PONG = 1
RESPONSE_CHANNEL_MESSAGE = 4
RESPONSE_DEFERRED_CHANNEL_MESSAGE = 5 # ← clave para AI
@app.post("/discord/interactions")
async def discord_interactions(request: Request, background_tasks: BackgroundTasks):
body = await request.body()
signature = request.headers.get("X-Signature-Ed25519", "")
timestamp = request.headers.get("X-Signature-Timestamp", "")
if not verify_discord_signature(body, signature, timestamp):
return JSONResponse({"error": "invalid signature"}, status_code=401)
payload = json.loads(body)
interaction_type = payload["type"]
# PING (verificación inicial)
if interaction_type == INTERACTION_PING:
return JSONResponse({"type": RESPONSE_PONG})
# Application command (slash command)
if interaction_type == INTERACTION_APPLICATION_COMMAND:
command_name = payload["data"]["name"]
if command_name == "ask":
question = payload["data"]["options"][0]["value"]
user_id = payload["member"]["user"]["id"]
# Encolar processing async
background_tasks.add_task(
process_ask_command,
payload["application_id"],
payload["token"], # interaction token, válido 15min para follow-ups
question,
user_id,
)
# Respond inmediato con "deferred" — usuario ve "Bot is thinking..."
return JSONResponse({
"type": RESPONSE_DEFERRED_CHANNEL_MESSAGE,
})
# Message component (button, select menu)
if interaction_type == INTERACTION_MESSAGE_COMPONENT:
custom_id = payload["data"]["custom_id"]
if custom_id == "feedback_helpful":
return JSONResponse({
"type": RESPONSE_CHANNEL_MESSAGE,
"data": {"content": "¡Gracias por el feedback!", "flags": 64}, # 64 = ephemeral
})
return JSONResponse({"type": RESPONSE_PONG})
async def process_ask_command(application_id: str, interaction_token: str, question: str, user_id: str):
"""Procesa async y manda follow-up via webhook."""
answer = await call_llm_with_rag(question)
# Mandá follow-up vía webhook (no requiere bot token, usa interaction_token)
async with httpx.AsyncClient() as client:
await client.patch(
f"https://discord.com/api/v10/webhooks/{application_id}/{interaction_token}/messages/@original",
json={
"content": answer,
"components": [
{
"type": 1, # action row
"components": [
{"type": 2, "style": 3, "label": "👍 Útil",
"custom_id": "feedback_helpful"},
{"type": 2, "style": 4, "label": "👎 No útil",
"custom_id": "feedback_not_helpful"},
],
}
],
},
)
Insight clave: RESPONSE_DEFERRED_CHANNEL_MESSAGE es tu salvación. Acknowledge inmediato pero el usuario ve "Bot is thinking..." mientras procesás async. Después editás el mensaje con PATCH al webhook de la interaction.
Registrar slash commands
Los comandos los registrás via API antes de que estén disponibles:
import httpx
BOT_TOKEN = os.environ["DISCORD_BOT_TOKEN"]
APPLICATION_ID = os.environ["DISCORD_APPLICATION_ID"]
commands = [
{
"name": "ask",
"description": "Pregúntale al AI assistant",
"options": [
{
"name": "question",
"description": "Tu pregunta",
"type": 3, # STRING
"required": True,
}
],
}
]
# Global (puede tardar 1 hora en propagar a todos los servers)
httpx.put(
f"https://discord.com/api/v10/applications/{APPLICATION_ID}/commands",
headers={"Authorization": f"Bot {BOT_TOKEN}"},
json=commands,
)
# Para testing rápido, registrá a un guild específico (instantáneo):
GUILD_ID = "..."
httpx.put(
f"https://discord.com/api/v10/applications/{APPLICATION_ID}/guilds/{GUILD_ID}/commands",
headers={"Authorization": f"Bot {BOT_TOKEN}"},
json=commands,
)
Gateway: cuando lo necesitás
Si tu bot necesita escuchar mensajes regulares (no solo comandos), necesitás Gateway. La librería estándar Python es discord.py:
# discord_bot.py
import discord
import os
intents = discord.Intents.default()
intents.message_content = True # privileged intent — requiere aprobación si bot >100 servers
class AIBot(discord.Client):
async def on_ready(self):
print(f"Conectado como {self.user}")
async def on_message(self, message: discord.Message):
# Ignora mensajes propios
if message.author == self.user:
return
# Responde solo cuando lo mencionan
if self.user in message.mentions:
await message.channel.typing() # "bot is typing..."
response = await call_llm_with_rag(message.content)
await message.reply(response)
bot = AIBot(intents=intents)
bot.run(os.environ["DISCORD_BOT_TOKEN"])
Este proceso debe estar siempre corriendo. Cae el bot, se desconecta.
Implicaciones de arquitectura
| Característica | Gateway | Interactions |
|---|---|---|
| Hosting | Proceso siempre vivo (VM, container) | Serverless OK (Lambda, Cloud Run) |
| Escala | Vertical primero, sharding después | Horizontal automático |
| Latencia recepción | Sub-segundo (push) | Sub-segundo también |
| Complejidad | Mayor (manejar reconexiones, heartbeats) | Menor |
| Failover | Si proceso cae, eventos se pierden hasta reconectar | Sin estado, otra instancia toma el siguiente |
Sharding: cuando crecés a muchos servers
Un proceso de bot puede manejar hasta ~2500 servers. Después necesitás sharding: dividir los servers entre N procesos.
# bot.py
import discord
# Auto-sharding: discord.py decide cuántos shards según el número de guilds
bot = discord.AutoShardedClient(intents=intents)
# Manual sharding si querés control
# bot = discord.Client(shard_count=4, shard_id=0, intents=intents)
Cada shard maneja un subset de servers. Si tenés bot popular (1000+ servers), necesitás esto. Para bots privados de empresas (1-10 servers), no.
Mensajes ricos: Embeds y Components
Discord no tiene Block Kit como Slack. Tiene Embeds (cards visuales) y Components (botones, selects, modals).
Embed (mensaje visual rico)
embed = {
"title": "Respuesta a tu pregunta",
"description": "**REST** es un estilo arquitectónico para APIs.",
"color": 0x5865F2, # Discord blurple
"fields": [
{"name": "Características", "value": "• Stateless\n• HTTP methods\n• URLs as resources", "inline": False},
],
"footer": {"text": "Respuesta generada por AI Assistant"},
"timestamp": "2026-05-11T15:00:00Z",
}
# En la respuesta de interaction:
{
"type": RESPONSE_CHANNEL_MESSAGE,
"data": {
"embeds": [embed],
"components": [...]
}
}
Components (botones, selects)
components = [
{
"type": 1, # ACTION_ROW
"components": [
{
"type": 2, # BUTTON
"style": 3, # SUCCESS (verde)
"label": "👍 Útil",
"custom_id": "feedback_helpful",
},
{
"type": 2,
"style": 4, # DANGER (rojo)
"label": "👎 No útil",
"custom_id": "feedback_not_helpful",
},
],
},
{
"type": 1,
"components": [
{
"type": 3, # SELECT_MENU
"custom_id": "topic_selector",
"placeholder": "Selecciona un tema",
"options": [
{"label": "REST", "value": "rest", "description": "API design"},
{"label": "GraphQL", "value": "graphql", "description": "Query language"},
],
}
],
},
]
Modals
Para input estructurado (formularios), Discord tiene modals:
# Cuando usuario click "feedback no útil"
{
"type": 9, # MODAL response type
"data": {
"title": "¿Qué podemos mejorar?",
"custom_id": "feedback_form",
"components": [
{
"type": 1,
"components": [
{
"type": 4, # TEXT_INPUT
"custom_id": "improvement",
"label": "Cuéntanos más",
"style": 2, # paragraph (multi-line)
"min_length": 5,
"max_length": 500,
}
]
}
],
},
}
Trampas comunes
Trampa 1 — Asumir que es como Slack. API distinta, modelo distinto, terminología distinta. Lo que aprendiste de Slack te sirve de framework, pero implementación es independiente.
Trampa 2 — Usar Gateway cuando no hace falta. Si tu bot solo necesita comandos, Interactions es serverless-friendly y mucho más simple. No te metas en Gateway si no es necesario.
Trampa 3 — Privileged Intents sin pedir aprobación.
MESSAGE_CONTENT intent (necesario para leer texto de mensajes regulares) requiere aprobación de Discord si tu bot está en >100 servers. Pedila con tiempo si vas a crecer.
Trampa 4 — Ed25519 vs HMAC.
Discord usa Ed25519, no HMAC-SHA256 como Slack. Librerías distintas (pynacl vs hashlib). Confundir esto te da signature siempre fallida.
Trampa 5 — Olvidar DEFERRED_CHANNEL_MESSAGE.
3s timeout + LLM lento. Sin defer, Discord te da error "interaction failed". Siempre defer si tu processing es >2s.
Trampa 6 — No registrar comandos en el guild durante development. Comandos globales tardan ~1 hora en propagar. Para iterar rápido, registrá al guild de testing (instantáneo).
Ejercicio
Diseñá tu bot Discord para este caso:
Requisitos:
- Bot privado para una empresa de ~80 personas en su Discord server
- Responde a
/ask <question>con info de knowledge base interna - Botones 👍/👎 para feedback
- También quiere que respondá cuando lo @mencionan en canales
Especificá:
- ¿Necesitás Gateway, Interactions, o ambos?
- ¿Cómo manejás los 3 segundos de timeout?
- ¿Cómo distinguís entre command
/asky mention en canal? - ¿Qué intents necesitás?
Ver solución
- Ambos:
- Interactions API para
/asky botones (más simple, serverless-friendly) - Gateway para detectar @mentions en canales (no hay otra forma)
- Interactions API para
- 3 segundos:
- Interactions: responder con
DEFERRED_CHANNEL_MESSAGE(mostrar "thinking..."), después PATCH al webhook con respuesta - Gateway: no hay timeout estricto, pero buena UX = empezar
typing()rápido y responder cuando tengas la respuesta
- Interactions: responder con
- Distinción:
/ask→ llega al endpoint/discord/interactionscon type=APPLICATION_COMMAND- @mention → llega al proceso Gateway via
on_messageevent, chequeásself.user in message.mentions
- Intents:
defaultpara conexión básicamessage_content(privileged) para leer el texto del mensaje cuando te mencionan- No necesitás intents privilegiados para
/asksolo
Resumen
Aprendiste:
- ✅ Discord tiene dos APIs: Gateway (WebSocket persistente) e Interactions API (HTTP)
- ✅ Cuándo cada una (Interactions para commands/buttons; Gateway para mensajes regulares)
- ✅ Setup, registro de slash commands, signature verification (Ed25519)
- ✅
DEFERRED_CHANNEL_MESSAGEpara procesamiento async >3s - ✅ Embeds + Components (botones, selects, modals)
- ✅ Sharding cuando creces a muchos servers
- ✅ Cuándo necesitás privileged intents
Checkpoint: si podés dibujar el flujo de Interactions API y entendés cuándo necesitarías Gateway, estás listo.
Siguiente cápsula
04 — OAuth 2.0 para bots. Hasta acá asumimos un bot token estático. Para distribuir tu bot a múltiples workspaces (Slack) o servers (Discord), necesitás OAuth flow: el cliente autoriza tu app, vos recibís tokens, guardás per-tenant. Vas a diseñar la arquitectura multi-tenant.
Recursos
- Discord Developer Docs — referencia oficial.
- discord.py — librería principal Python para Gateway.
- Discord Interactions API — para slash commands y components.
- Discord Sharding Guide — cuándo y cómo shardear.
- pynacl — librería Ed25519 para signature verification.