O maior portal de MU Online do Brasil — desde 2003
Tutorial Intermediário Website

Como integrar o Discord (webhook de eventos) ao servidor de MU

Conecte o servidor de MU Online ao Discord via webhooks para anunciar invasões, resets, novos registros e status do servidor automaticamente, com embeds bonitos, rate limit e fila de mensagens.

GA Gabriel · Atualizado em 14 jul 2025 · ⏱ 23 min de leitura
Resposta rápida

Integrar o servidor de MU Online ao Discord transforma o canal da comunidade em um mural vivo: cada invasão anunciada, cada reset comemorado, cada novo jogador registrado aparece automaticamente, sem ninguém digitar nada. A forma mais direta e robusta de fazer isso é com webhooks — URLs que recebem

Integrar o servidor de MU Online ao Discord transforma o canal da comunidade em um mural vivo: cada invasão anunciada, cada reset comemorado, cada novo jogador registrado aparece automaticamente, sem ninguém digitar nada. A forma mais direta e robusta de fazer isso é com webhooks — URLs que recebem um POST em JSON e publicam a mensagem no canal. Neste tutorial você vai criar o webhook, montar uma biblioteca PHP reutilizável com embeds, ligar os disparos a eventos reais do servidor e proteger tudo com fila e rate limit. Os valores concretos (cores, horários, campos do banco) aparecem como exemplo e variam por versão.

Pré-requisitos

Este guia parte de um servidor já funcional com site em PHP. Se você ainda está montando a base, veja como criar um servidor de MU Online e depois volte para plugar o Discord.

RequisitoDetalhe (exemplo — varia por versão)
Servidor DiscordUm servidor onde você tenha permissão de gerenciar webhooks
Canal de anúnciosEx.: #anuncios ou #eventos
PHP7.4+ com cURL habilitado
Acesso ao bancoLeitura da base do MU para detectar eventos
Cron ou agendadorPara checar eventos periodicamente

O único requisito não óbvio é o cURL habilitado no PHP, pois é com ele que fazemos o POST HTTPS para o Discord. Confirme com php -m | grep curl no Linux ou verificando a extensão no php.ini no Windows.

Passo 1 — Criar o webhook no Discord

O webhook é criado dentro do próprio Discord, não em código:

  1. Abra as configurações do canal desejado (engrenagem ao lado do nome do canal).
  2. Vá em Integrações e depois em Webhooks.
  3. Clique em Novo Webhook, dê um nome (ex.: "Servidor MU") e escolha o canal.
  4. Clique em Copiar URL do Webhook. Ela tem o formato https://discord.com/api/webhooks/ID/TOKEN.
  5. Guarde essa URL como um segredo — quem a tiver pode postar no seu canal.

Trate a URL do webhook como uma senha. Ela não deve aparecer em código versionado, em JavaScript no navegador nem em repositório público. Vamos guardá-la num arquivo de configuração protegido.

Passo 2 — Guardar a configuração fora do código

Crie um arquivo config/discord.php fora da raiz pública, ou pelo menos protegido de acesso direto. Ele guarda a URL e parâmetros gerais:

<?php
// config/discord.php  (mantenha fora do controle de versão)

return [
    // URL copiada do Discord — NUNCA exponha publicamente
    'webhook_url' => 'https://discord.com/api/webhooks/ID/TOKEN',

    // Nome e avatar padrão exibidos pelas mensagens do webhook
    'username'    => 'Servidor MU',
    'avatar_url'  => 'https://seusite.com/img/logo-discord.png',

    // Cores por tipo de evento (inteiro decimal — ver Passo 4)
    'cores' => [
        'invasao'  => 0xE67E22, // laranja
        'reset'    => 0x9B59B6, // roxo
        'registro' => 0x2ECC71, // verde
        'status'   => 0x3498DB, // azul
        'alerta'   => 0xE74C3C, // vermelho
    ],
];

Passo 3 — Biblioteca PHP para enviar ao webhook

Agora a peça central: uma função que faz o POST com cURL, respeita timeout e trata o rate limit (HTTP 429). Salve em lib/discord.php:

<?php
// lib/discord.php

/**
 * Envia um payload ao webhook do Discord.
 * Retorna o código HTTP; trata 429 (rate limit) esperando o 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: o Discord diz quanto esperar
        if ($codigo === 429) {
            $dados = json_decode($resposta, true);
            $espera = (float) ($dados['retry_after'] ?? 1);
            usleep((int) ($espera * 1_000_000) + 100_000);
            continue; // tenta de novo
        }

        // 2xx = sucesso (204 é o mais comum para webhook)
        if ($codigo >= 200 && $codigo < 300) {
            return $codigo;
        }

        // outro erro: pequena pausa e nova tentativa
        usleep(500_000);
    }

    return $codigo ?? 0;
}

O tratamento de HTTP 429 é o que impede sua integração de ser bloqueada. Quando você excede o limite, o Discord responde com retry_after em segundos — a função espera esse tempo e tenta de novo, em vez de martelar o endpoint.

Passo 4 — Montar embeds bonitos

Uma mensagem de texto simples cumpre a função, mas um embed — com cor, título, thumbnail e campos — dá aparência profissional. A cor é um inteiro decimal (o hexadecimal 0xE67E22 é laranja). Vamos criar um construtor:

<?php
// lib/discord.php (continuação)

/**
 * Monta um embed padronizado para eventos do 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, o Discord formata a hora
        'footer'      => ['text' => 'Servidor MU · Sistema automático'],
    ];

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

    // Campos aparecem em grade: [['name'=>..,'value'=>..,'inline'=>true], ...]
    if ($campos) {
        $embed['fields'] = $campos;
    }

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

Com isso, enviar um anúncio vira uma linha:

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

discordEnviar(discordEmbed(
    '⚔️ Invasão do Golden Dragon!',
    'A invasão começou em **Lorencia**. Corra para pegar os drops!',
    'invasao',
    [
        ['name' => 'Mapa',    'value' => 'Lorencia', 'inline' => true],
        ['name' => 'Duração', 'value' => '20 min',   'inline' => true],
    ],
    'https://seusite.com/img/golden.png'
));

Passo 5 — Detectar eventos do servidor

O ponto mais importante da arquitetura: você não altera o GameServer em C++. Em vez disso, o backend PHP detecta o evento e dispara o webhook. Existem três padrões de detecção, do mais simples ao mais integrado:

PadrãoComo funcionaQuando usar
AgendadoCron dispara nos horários fixos dos eventosInvasões e eventos de horário conhecido
Polling de bancoCron lê o banco e detecta mudanças (novos resets, registros)Resets, novos registros, marcos
Endpoint internoO site chama o webhook quando uma ação acontece no próprio siteRegistro de conta, compra de VIP

Exemplo: anunciar invasões por horário

Se seus eventos ocorrem em horários fixos (que variam por versão e pela sua config), um cron a cada minuto verifica se é hora de anunciar:

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

// Horários de EXEMPLO — ajuste aos do seu 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'] . ' começou!',
        'Invasão ativa em **' . $ev['mapa'] . '**. Boa caçada!',
        'invasao',
        [['name' => 'Mapa', 'value' => $ev['mapa'], 'inline' => true]]
    ));
}

Exemplo: anunciar novos resets por polling

Para comemorar quando alguém dá reset, guarde o último reset já anunciado e verifique o banco periodicamente:

<?php
// cron/anunciar-resets.php  (rode via cron a 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();

// Tabela/coluna de log de reset VARIAM POR VERSÃO — ajuste ao seu schema.
// Aqui supomos uma tabela 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(
        '🔄 Novo Reset!',
        '**' . $row['Name'] . '** alcançou o reset **' . $row['Resets'] . '**!',
        'reset',
        [['name' => 'Resets', 'value' => (string) $row['Resets'], 'inline' => true]]
    ));
    $maiorId = max($maiorId, (int) $row['id']);
    usleep(300_000); // pequeno respiro entre mensagens (rate limit)
}

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

O arquivo marcador é o que evita anunciar o mesmo reset duas vezes. Sem ele, cada execução do cron reenviaria tudo.

Exemplo: anunciar novo registro pelo site

Quando o evento acontece no próprio site (registro de conta), dispare direto no fluxo:

<?php
// dentro do seu processo de registro, após criar a conta com sucesso
require_once __DIR__ . '/lib/discord.php';

discordEnviar(discordEmbed(
    '🎉 Novo jogador!',
    'Bem-vindo(a) ao servidor! Total de contas cresce a cada dia.',
    'registro'
));

Passo 6 — Fila para respeitar o rate limit

Se vários eventos disparam ao mesmo tempo (dez resets no mesmo minuto), enviar tudo de uma vez estoura o rate limit. A solução profissional é uma fila: eventos entram numa tabela e um único worker envia com espaçamento.

-- Fila de mensagens do Discord (MySQL de exemplo)
CREATE TABLE discord_fila (
    id        INT AUTO_INCREMENT PRIMARY KEY,
    payload   TEXT      NOT NULL,     -- JSON do embed
    status    TINYINT   NOT NULL DEFAULT 0,  -- 0=pendente,1=enviado,2=erro
    criado_em DATETIME  NOT NULL DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_status (status, id)
);
<?php
// lib/discord-fila.php

// Enfileirar (chame no lugar de discordEnviar quando houver volume)
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 (rode via cron a cada 1 minuto) — envia com espaçamento
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 envios para não estourar o limite
    }
}

Com a fila, mesmo uma enxurrada de eventos vira um fluxo controlado de mensagens, e nenhuma é perdida por causa de HTTP 429.

Passo 7 — Testar a integração

Antes de deixar rodando, teste manualmente com um script simples:

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

$codigo = discordEnviar(discordEmbed(
    '✅ Teste de integração',
    'Se você está vendo isto no canal, o webhook está funcionando!',
    'status'
));

echo $codigo === 204 || ($codigo >= 200 && $codigo < 300)
    ? "OK — mensagem enviada (HTTP $codigo)\n"
    : "Falhou — HTTP $codigo\n";

Rode com php teste-discord.php. Um HTTP 204 significa sucesso (o Discord não retorna corpo em webhook bem-sucedido, por isso 204 e não 200).

Erros comuns e soluções

SintomaCausa provávelSolução
Nada aparece no canalURL do webhook errada ou canal excluídoRecopie a URL nas integrações do canal
HTTP 401/403Token do webhook inválido ou revogadoGere um novo webhook e atualize a config
HTTP 429 constanteMuitas mensagens em pouco tempoImplemente a fila e respeite retry_after
Acentos quebradosJSON sem JSON_UNESCAPED_UNICODEUse a flag ao codificar o payload
Embed sem corCor enviada como hexa stringEnvie a cor como inteiro decimal
Reset anunciado duas vezesMarcador de último ID não gravadoVerifique escrita no arquivo/tabela marcador
cURL falha silenciosamenteExtensão cURL desabilitadaHabilite curl no php.ini

Segurança e boas práticas

  • URL do webhook é segredo. Fora do código versionado, fora do navegador, fora de repositório público. Quem a tem, posta no seu canal.
  • Sempre trate o 429. Respeite o retry_after do Discord; nunca reenvie em loop cego.
  • Desacople o jogo do Discord. Detecte eventos pelo banco/site, não alterando o GameServer.
  • Use fila sob volume. Vários eventos simultâneos exigem espaçamento entre envios.
  • Idempotência nos polls. Um marcador de "último processado" evita anúncios duplicados.
  • Timeout curto no cURL. Um Discord lento não pode travar seu cron nem seu registro de conta.

Checklist de lançamento

  • Webhook criado e URL copiada com segurança
  • Config do Discord fora do código versionado
  • cURL habilitado no PHP confirmado
  • Função de envio tratando HTTP 429 com retry_after
  • Embeds com cor por tipo de evento
  • Cron de invasões nos horários reais do GameServer
  • Polling de resets com marcador de último ID
  • Disparo de registro ligado ao fluxo do site
  • Fila implementada para picos de eventos
  • Worker da fila rodando via cron com espaçamento
  • Teste manual retornando HTTP 204
  • Teste real: forçar um evento e confirmar mensagem no canal

Com a integração pronta, o Discord vira uma extensão automática do servidor. Cada invasão vira uma chamada para a ação, cada reset vira uma comemoração pública e cada novo jogador é recebido pela comunidade — tudo sem esforço manual, com código que respeita os limites do Discord e nunca perde uma mensagem.

Perguntas frequentes

Webhook ou bot: qual usar para anunciar eventos?

Para enviar mensagens automáticas de eventos, o webhook é mais simples e não exige manter um bot online. O bot só é necessário quando você precisa que o Discord reaja a comandos ou leia mensagens. Para anúncios de invasão, reset e status, o webhook resolve com muito menos esforço.

A URL do webhook pode ficar no código do site?

Ela deve ficar fora do código versionado, num arquivo de configuração protegido ou variável de ambiente. Quem tiver a URL pode postar no seu canal, então trate-a como segredo. Nunca a exponha em JavaScript no navegador nem em repositório público.

Com que frequência posso enviar mensagens ao webhook?

O Discord aplica rate limit. Como referência, evite passar de cerca de 5 requisições por poucos segundos no mesmo webhook; os limites exatos variam e o Discord retorna HTTP 429 com um tempo de espera quando você excede. Implemente uma fila e respeite o cabeçalho de retry.

Como faço o servidor de jogo disparar o webhook se ele é em C++?

Você raramente altera o GameServer diretamente. O padrão é o site/backend detectar o evento (por leitura do banco, log ou um endpoint) e disparar o webhook em PHP. Assim você desacopla o jogo do Discord e evita mexer no core do servidor.

Consigo enviar embeds com cor, imagem e campos?

Sim. O webhook aceita um array embeds com título, descrição, cor (inteiro decimal), thumbnail, campos e rodapé. É o que deixa o anúncio com aparência profissional em vez de um texto simples.

GA
Editor de guias e builds

Gabriel cobre gameplay, builds de classes, PvP e progressão. Testa cada estratégia em servidor antes de publicar.

Continue lendo

Artigos relacionados