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 canalesGateway
Solo responder a /comandos y botonesInteractions API
Detectar nuevos miembros, reactions, presenceGateway
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

En Discord Developer Portal:

  1. Create application
  2. Bot section: crea bot, obtené el token (MzU0OTE0...)
  3. OAuth2 → URL Generator: scopes bot y applications.commands, permisos relevantes
  4. 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ísticaGatewayInteractions
HostingProceso siempre vivo (VM, container)Serverless OK (Lambda, Cloud Run)
EscalaVertical primero, sharding despuésHorizontal automático
Latencia recepciónSub-segundo (push)Sub-segundo también
ComplejidadMayor (manejar reconexiones, heartbeats)Menor
FailoverSi proceso cae, eventos se pierden hasta reconectarSin 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á:

  1. ¿Necesitás Gateway, Interactions, o ambos?
  2. ¿Cómo manejás los 3 segundos de timeout?
  3. ¿Cómo distinguís entre command /ask y mention en canal?
  4. ¿Qué intents necesitás?
Ver solución
  1. Ambos:
    • Interactions API para /ask y botones (más simple, serverless-friendly)
    • Gateway para detectar @mentions en canales (no hay otra forma)
  2. 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
  3. Distinción:
    • /ask → llega al endpoint /discord/interactions con type=APPLICATION_COMMAND
    • @mention → llega al proceso Gateway via on_message event, chequeás self.user in message.mentions
  4. Intents:
    • default para conexión básica
    • message_content (privileged) para leer el texto del mensaje cuando te mencionan
    • No necesitás intents privilegiados para /ask solo

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_MESSAGE para 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

  1. Discord Developer Docs — referencia oficial.
  2. discord.py — librería principal Python para Gateway.
  3. Discord Interactions API — para slash commands y components.
  4. Discord Sharding Guide — cuándo y cómo shardear.
  5. pynacl — librería Ed25519 para signature verification.