El mayor portal de MU Online de Brasil — desde 2003
Tutorial Intermedio Web

Cómo integrar Discord (webhook de eventos) al servidor de MU

Conecta el servidor de MU Online a Discord mediante webhooks para anunciar invasiones, resets, nuevos registros y estado del servidor automáticamente, con embeds bonitos, rate limit y cola de mensajes.

GA Gabriel · Actualizado el 14 jul 2025 · ⏱ 23 min de lectura
Respuesta rápida

Integrar el servidor de MU Online a Discord transforma el canal de la comunidad en un mural vivo: cada invasión anunciada, cada reset celebrado, cada nuevo jugador registrado aparece automáticamente, sin que nadie escriba nada. La forma más directa y robusta de hacerlo es con webhooks: URLs que reci

Integrar el servidor de MU Online a Discord transforma el canal de la comunidad en un mural vivo: cada invasión anunciada, cada reset celebrado, cada nuevo jugador registrado aparece automáticamente, sin que nadie escriba nada. La forma más directa y robusta de hacerlo es con webhooks: URLs que reciben un POST en JSON y publican el mensaje en el canal. En este tutorial vas a crear el webhook, armar una biblioteca PHP reutilizable con embeds, ligar los disparos a eventos reales del servidor y protegerlo todo con cola y rate limit. Los valores concretos (colores, horarios, campos de la base) aparecen como ejemplo y varían según la versión.

Requisitos previos

Esta guía parte de un servidor ya funcional con sitio en PHP. Si todavía estás armando la base, mira cómo crear un servidor de MU Online y luego vuelve para enchufar Discord.

RequisitoDetalle (ejemplo — varía según la versión)
Servidor DiscordUn servidor donde tengas permiso de gestionar webhooks
Canal de anunciosEj.: #anuncios o #eventos
PHP7.4+ con cURL habilitado
Acceso a la baseLectura de la base de MU para detectar eventos
Cron o programadorPara chequear eventos periódicamente

El único requisito no obvio es cURL habilitado en PHP, ya que es con él que hacemos el POST HTTPS a Discord. Confírmalo con php -m | grep curl en Linux o verificando la extensión en el php.ini en Windows.

Paso 1 — Crear el webhook en Discord

El webhook se crea dentro del propio Discord, no en código:

  1. Abre la configuración del canal deseado (el engranaje al lado del nombre del canal).
  2. Ve a Integraciones y luego a Webhooks.
  3. Haz clic en Nuevo Webhook, dale un nombre (ej.: "Servidor MU") y elige el canal.
  4. Haz clic en Copiar URL del Webhook. Tiene el formato https://discord.com/api/webhooks/ID/TOKEN.
  5. Guarda esa URL como un secreto: quien la tenga puede postear en tu canal.

Trata la URL del webhook como una contraseña. No debe aparecer en código versionado, en JavaScript en el navegador ni en un repositorio público. Vamos a guardarla en un archivo de configuración protegido.

Paso 2 — Guardar la configuración fuera del código

Crea un archivo config/discord.php fuera de la raíz pública, o al menos protegido de acceso directo. Guarda la URL y los parámetros generales:

<?php
// config/discord.php  (mantener fuera del control de versiones)

return [
    // URL copiada de Discord — NUNCA la expongas públicamente
    'webhook_url' => 'https://discord.com/api/webhooks/ID/TOKEN',

    // Nombre y avatar por defecto mostrados por los mensajes del webhook
    'username'    => 'Servidor MU',
    'avatar_url'  => 'https://tusitio.com/img/logo-discord.png',

    // Colores por tipo de evento (entero decimal — ver Paso 4)
    'cores' => [
        'invasao'  => 0xE67E22, // naranja
        'reset'    => 0x9B59B6, // morado
        'registro' => 0x2ECC71, // verde
        'status'   => 0x3498DB, // azul
        'alerta'   => 0xE74C3C, // rojo
    ],
];

Paso 3 — Biblioteca PHP para enviar al webhook

Ahora la pieza central: una función que hace el POST con cURL, respeta el timeout y trata el rate limit (HTTP 429). Guárdala en lib/discord.php:

<?php
// lib/discord.php

/**
 * Envía un payload al webhook de Discord.
 * Devuelve el código HTTP; trata el 429 (rate limit) esperando el retry_after.
 */
function discordEnviar(array $payload, int $tentativas = 3): int {
    $cfg = require __DIR__ . '/../config/discord.php';

    $payload['username']   = $payload['username']   ?? $cfg['username'];
    $payload['avatar_url'] = $payload['avatar_url'] ?? $cfg['avatar_url'];

    $json = json_encode($payload, JSON_UNESCAPED_UNICODE);

    for ($i = 0; $i < $tentativas; $i++) {
        $ch = curl_init($cfg['webhook_url']);
        curl_setopt_array($ch, [
            CURLOPT_POST           => true,
            CURLOPT_POSTFIELDS     => $json,
            CURLOPT_HTTPHEADER     => ['Content-Type: application/json'],
            CURLOPT_RETURNTRANSFER => true,
            CURLOPT_TIMEOUT        => 8,
            CURLOPT_CONNECTTIMEOUT => 4,
        ]);
        $resposta = curl_exec($ch);
        $codigo   = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
        curl_close($ch);

        // 429 = rate limit: Discord dice cuánto esperar
        if ($codigo === 429) {
            $dados = json_decode($resposta, true);
            $espera = (float) ($dados['retry_after'] ?? 1);
            usleep((int) ($espera * 1_000_000) + 100_000);
            continue; // intenta de nuevo
        }

        // 2xx = éxito (204 es el más común para webhook)
        if ($codigo >= 200 && $codigo < 300) {
            return $codigo;
        }

        // otro error: pequeña pausa y nuevo intento
        usleep(500_000);
    }

    return $codigo ?? 0;
}

El tratamiento del HTTP 429 es lo que impide que tu integración sea bloqueada. Cuando excedes el límite, Discord responde con retry_after en segundos: la función espera ese tiempo e intenta de nuevo, en vez de martillar el endpoint.

Paso 4 — Armar embeds bonitos

Un mensaje de texto simple cumple la función, pero un embed —con color, título, thumbnail y campos— da una apariencia profesional. El color es un entero decimal (el hexadecimal 0xE67E22 es naranja). Vamos a crear un constructor:

<?php
// lib/discord.php (continuación)

/**
 * Arma un embed estandarizado para eventos del servidor.
 */
function discordEmbed(
    string $titulo,
    string $descricao,
    string $tipo = 'status',
    array  $campos = [],
    ?string $thumb = null
): array {
    $cfg = require __DIR__ . '/../config/discord.php';
    $cor = $cfg['cores'][$tipo] ?? 0x95A5A6;

    $embed = [
        'title'       => $titulo,
        'description' => $descricao,
        'color'       => $cor,
        'timestamp'   => date('c'), // ISO 8601, Discord formatea la hora
        'footer'      => ['text' => 'Servidor MU · Sistema automático'],
    ];

    if ($thumb) {
        $embed['thumbnail'] = ['url' => $thumb];
    }

    // Los campos aparecen en grilla: [['name'=>..,'value'=>..,'inline'=>true], ...]
    if ($campos) {
        $embed['fields'] = $campos;
    }

    return ['embeds' => [$embed]];
}

Con esto, enviar un anuncio se vuelve una línea:

<?php
require_once __DIR__ . '/lib/discord.php';

discordEnviar(discordEmbed(
    '⚔️ ¡Invasión del Golden Dragon!',
    'La invasión comenzó en **Lorencia**. ¡Corre a agarrar los drops!',
    'invasao',
    [
        ['name' => 'Mapa',     'value' => 'Lorencia', 'inline' => true],
        ['name' => 'Duración', 'value' => '20 min',   'inline' => true],
    ],
    'https://tusitio.com/img/golden.png'
));

Paso 5 — Detectar eventos del servidor

El punto más importante de la arquitectura: no alteras el GameServer en C++. En su lugar, el backend PHP detecta el evento y dispara el webhook. Existen tres patrones de detección, del más simple al más integrado:

PatrónCómo funcionaCuándo usarlo
AgendadoEl cron dispara en los horarios fijos de los eventosInvasiones y eventos de horario conocido
Polling de baseEl cron lee la base y detecta cambios (nuevos resets, registros)Resets, nuevos registros, hitos
Endpoint internoEl sitio llama al webhook cuando una acción sucede en el propio sitioRegistro de cuenta, compra de VIP

Ejemplo: anunciar invasiones por horario

Si tus eventos ocurren en horarios fijos (que varían según la versión y tu config), un cron cada minuto verifica si es hora de anunciar:

<?php
// cron/anunciar-invasoes.php  (ejecutar vía cron cada 1 minuto)
require_once __DIR__ . '/../lib/discord.php';

// Horarios de EJEMPLO — ajústalos a los de tu GameServer
$agenda = [
    '20:00' => ['nome' => 'Golden Invasion', 'mapa' => 'Devias'],
    '22:00' => ['nome' => 'Golden Invasion', 'mapa' => 'Noria'],
    '21:00' => ['nome' => 'Red Dragon',      'mapa' => 'Atlans'],
];

$horaAtual = date('H:i');
if (isset($agenda[$horaAtual])) {
    $ev = $agenda[$horaAtual];
    discordEnviar(discordEmbed(
        '🐉 ¡' . $ev['nome'] . ' comenzó!',
        'Invasión activa en **' . $ev['mapa'] . '**. ¡Buena cacería!',
        'invasao',
        [['name' => 'Mapa', 'value' => $ev['mapa'], 'inline' => true]]
    ));
}

Ejemplo: anunciar nuevos resets por polling

Para celebrar cuando alguien hace reset, guarda el último reset ya anunciado y verifica la base periódicamente:

<?php
// cron/anunciar-resets.php  (ejecutar vía cron cada 2 minutos)
require_once __DIR__ . '/../lib/discord.php';
require_once __DIR__ . '/../lib/db.php'; // getMuDB()

$marcador = sys_get_temp_dir() . '/ultimo_reset_id.txt';
$ultimoId = is_file($marcador) ? (int) file_get_contents($marcador) : 0;

$conn = getMuDB();

// La tabla/columna de log de reset VARÍAN SEGÚN LA VERSIÓN — ajústalas a tu schema.
// Aquí suponemos una tabla ResetLog(id, Name, Resets, DataReset).
$sql = "SELECT TOP 10 id, Name, Resets
        FROM ResetLog
        WHERE id > ?
        ORDER BY id ASC";
$stmt = sqlsrv_query($conn, $sql, [[$ultimoId, SQLSRV_PARAM_IN]]);

$maiorId = $ultimoId;
while ($row = sqlsrv_fetch_array($stmt, SQLSRV_FETCH_ASSOC)) {
    discordEnviar(discordEmbed(
        '🔄 ¡Nuevo Reset!',
        '¡**' . $row['Name'] . '** alcanzó el reset **' . $row['Resets'] . '**!',
        'reset',
        [['name' => 'Resets', 'value' => (string) $row['Resets'], 'inline' => true]]
    ));
    $maiorId = max($maiorId, (int) $row['id']);
    usleep(300_000); // pequeño respiro entre mensajes (rate limit)
}

file_put_contents($marcador, (string) $maiorId);

El archivo marcador es lo que evita anunciar el mismo reset dos veces. Sin él, cada ejecución del cron reenviaría todo.

Ejemplo: anunciar un nuevo registro desde el sitio

Cuando el evento sucede en el propio sitio (registro de cuenta), dispáralo directo en el flujo:

<?php
// dentro de tu proceso de registro, tras crear la cuenta con éxito
require_once __DIR__ . '/lib/discord.php';

discordEnviar(discordEmbed(
    '🎉 ¡Nuevo jugador!',
    '¡Bienvenido(a) al servidor! El total de cuentas crece cada día.',
    'registro'
));

Paso 6 — Cola para respetar el rate limit

Si varios eventos disparan al mismo tiempo (diez resets en el mismo minuto), enviarlo todo de una vez revienta el rate limit. La solución profesional es una cola: los eventos entran en una tabla y un único worker envía con espaciado.

-- Cola de mensajes de Discord (MySQL de ejemplo)
CREATE TABLE discord_fila (
    id        INT AUTO_INCREMENT PRIMARY KEY,
    payload   TEXT      NOT NULL,     -- JSON del embed
    status    TINYINT   NOT NULL DEFAULT 0,  -- 0=pendiente,1=enviado,2=error
    criado_em DATETIME  NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_status (status, id)
);
<?php
// lib/discord-fila.php

// Encolar (llámalo en lugar de discordEnviar cuando haya volumen)
function discordEnfileirar(array $payload): void {
    $pdo = getSiteDB();
    $pdo->prepare("INSERT INTO discord_fila (payload) VALUES (:p)")
        ->execute([':p' => json_encode($payload, JSON_UNESCAPED_UNICODE)]);
}

// Worker (ejecutar vía cron cada 1 minuto) — envía con espaciado
function discordProcessarFila(int $maxPorRodada = 8): void {
    $pdo = getSiteDB();
    $itens = $pdo->query(
        "SELECT id, payload FROM discord_fila WHERE status = 0 ORDER BY id ASC LIMIT $maxPorRodada"
    )->fetchAll(PDO::FETCH_ASSOC);

    foreach ($itens as $item) {
        $codigo = discordEnviar(json_decode($item['payload'], true));
        $novoStatus = ($codigo >= 200 && $codigo < 300) ? 1 : 2;
        $pdo->prepare("UPDATE discord_fila SET status = :s WHERE id = :id")
            ->execute([':s' => $novoStatus, ':id' => $item['id']]);
        usleep(400_000); // ~0,4s entre envíos para no reventar el límite
    }
}

Con la cola, incluso una avalancha de eventos se vuelve un flujo controlado de mensajes, y ninguna se pierde por culpa del HTTP 429.

Paso 7 — Probar la integración

Antes de dejarlo corriendo, pruébalo manualmente con un script simple:

<?php
// teste-discord.php
require_once __DIR__ . '/lib/discord.php';

$codigo = discordEnviar(discordEmbed(
    '✅ Prueba de integración',
    '¡Si estás viendo esto en el canal, el webhook está funcionando!',
    'status'
));

echo $codigo === 204 || ($codigo >= 200 && $codigo < 300)
    ? "OK — mensaje enviado (HTTP $codigo)\n"
    : "Falló — HTTP $codigo\n";

Ejecútalo con php teste-discord.php. Un HTTP 204 significa éxito (Discord no devuelve cuerpo en un webhook exitoso, por eso 204 y no 200).

Errores comunes y soluciones

SíntomaCausa probableSolución
No aparece nada en el canalURL del webhook equivocada o canal eliminadoRecopia la URL en las integraciones del canal
HTTP 401/403Token del webhook inválido o revocadoGenera un nuevo webhook y actualiza la config
HTTP 429 constanteMuchos mensajes en poco tiempoImplementa la cola y respeta retry_after
Acentos rotosJSON sin JSON_UNESCAPED_UNICODEUsa la flag al codificar el payload
Embed sin colorColor enviado como string hexaEnvía el color como entero decimal
Reset anunciado dos vecesMarcador de último ID no grabadoVerifica la escritura en el archivo/tabla marcador
cURL falla silenciosamenteExtensión cURL deshabilitadaHabilita curl en el php.ini

Seguridad y buenas prácticas

  • La URL del webhook es un secreto. Fuera del código versionado, fuera del navegador, fuera de un repositorio público. Quien la tiene, postea en tu canal.
  • Siempre trata el 429. Respeta el retry_after de Discord; nunca reenvíes en un loop ciego.
  • Desacopla el juego de Discord. Detecta los eventos por la base/sitio, no alterando el GameServer.
  • Usa cola bajo volumen. Varios eventos simultáneos exigen espaciado entre envíos.
  • Idempotencia en los polls. Un marcador de "último procesado" evita anuncios duplicados.
  • Timeout corto en cURL. Un Discord lento no puede trabar tu cron ni tu registro de cuenta.

Lista de verificación de lanzamiento

  • Webhook creado y URL copiada con seguridad
  • Config de Discord fuera del código versionado
  • cURL habilitado en PHP confirmado
  • Función de envío tratando el HTTP 429 con retry_after
  • Embeds con color por tipo de evento
  • Cron de invasiones en los horarios reales del GameServer
  • Polling de resets con marcador de último ID
  • Disparo de registro ligado al flujo del sitio
  • Cola implementada para picos de eventos
  • Worker de la cola corriendo vía cron con espaciado
  • Prueba manual devolviendo HTTP 204
  • Prueba real: forzar un evento y confirmar el mensaje en el canal

Con la integración lista, Discord se vuelve una extensión automática del servidor. Cada invasión se vuelve un llamado a la acción, cada reset se vuelve una celebración pública y cada nuevo jugador es recibido por la comunidad, todo sin esfuerzo manual, con código que respeta los límites de Discord y nunca pierde un mensaje.

Preguntas frecuentes

¿Webhook o bot: cuál usar para anunciar eventos?

Para enviar mensajes automáticas de eventos, el webhook es más simple y no exige mantener un bot online. El bot solo es necesario cuando necesitas que Discord reaccione a comandos o lea mensajes. Para anuncios de invasión, reset y estado, el webhook resuelve con mucho menos esfuerzo.

¿La URL del webhook puede quedar en el código del sitio?

Debe quedar fuera del código versionado, en un archivo de configuración protegido o en una variable de entorno. Quien tenga la URL puede postear en tu canal, así que trátala como un secreto. Nunca la expongas en JavaScript en el navegador ni en un repositorio público.

¿Con qué frecuencia puedo enviar mensajes al webhook?

Discord aplica rate limit. Como referencia, evita pasar de unas 5 peticiones en pocos segundos en el mismo webhook; los límites exactos varían y Discord devuelve HTTP 429 con un tiempo de espera cuando lo excedes. Implementa una cola y respeta el encabezado de retry.

¿Cómo hago que el servidor de juego dispare el webhook si es en C++?

Rara vez alteras el GameServer directamente. El patrón es que el sitio/backend detecte el evento (por lectura de la base, log o un endpoint) y dispare el webhook en PHP. Así desacoplas el juego de Discord y evitas tocar el core del servidor.

¿Puedo enviar embeds con color, imagen y campos?

Sí. El webhook acepta un array embeds con título, descripción, color (entero decimal), thumbnail, campos y pie de página. Es lo que le da al anuncio una apariencia profesional en vez de un texto simple.

GA
Editor de guías y builds

Gabriel cubre gameplay, builds de clases, PvP y progresión. Prueba cada estrategia en un servidor antes de publicar.

Sigue leyendo

Artículos relacionados