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.
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.
| Requisito | Detalhe (exemplo — varia por versão) |
|---|---|
| Servidor Discord | Um servidor onde você tenha permissão de gerenciar webhooks |
| Canal de anúncios | Ex.: #anuncios ou #eventos |
| PHP | 7.4+ com cURL habilitado |
| Acesso ao banco | Leitura da base do MU para detectar eventos |
| Cron ou agendador | Para 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:
- Abra as configurações do canal desejado (engrenagem ao lado do nome do canal).
- Vá em Integrações e depois em Webhooks.
- Clique em Novo Webhook, dê um nome (ex.: "Servidor MU") e escolha o canal.
- Clique em Copiar URL do Webhook. Ela tem o formato
https://discord.com/api/webhooks/ID/TOKEN. - 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ão | Como funciona | Quando usar |
|---|---|---|
| Agendado | Cron dispara nos horários fixos dos eventos | Invasões e eventos de horário conhecido |
| Polling de banco | Cron lê o banco e detecta mudanças (novos resets, registros) | Resets, novos registros, marcos |
| Endpoint interno | O site chama o webhook quando uma ação acontece no próprio site | Registro 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
| Sintoma | Causa provável | Solução |
|---|---|---|
| Nada aparece no canal | URL do webhook errada ou canal excluído | Recopie a URL nas integrações do canal |
| HTTP 401/403 | Token do webhook inválido ou revogado | Gere um novo webhook e atualize a config |
| HTTP 429 constante | Muitas mensagens em pouco tempo | Implemente a fila e respeite retry_after |
| Acentos quebrados | JSON sem JSON_UNESCAPED_UNICODE | Use a flag ao codificar o payload |
| Embed sem cor | Cor enviada como hexa string | Envie a cor como inteiro decimal |
| Reset anunciado duas vezes | Marcador de último ID não gravado | Verifique escrita no arquivo/tabela marcador |
| cURL falha silenciosamente | Extensão cURL desabilitada | Habilite 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_afterdo 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.