Retour au cours

games / fivem

Intégration Discord : webhooks et bot de logs

Leçon 211 exercice

Explication

Ce que vous allez apprendre

  • Envoyer un message formaté (embed) vers Discord avec PerformHttpRequest
  • Organiser plusieurs webhooks distincts par catégorie de log (bans, économie, admin)
  • Respecter les limites de taux imposées par Discord avec une file d'attente
  • Gérer le code 429 (rate limit dépassé) sans perdre silencieusement un log
  • Sécuriser une route exposée à un bot externe avec le système ACE déjà vu

Dans quel contexte ?

Une équipe de modération d'un serveur RP ne peut pas rester connectée en permanence à la console du serveur pour surveiller les bannissements ou les anomalies économiques. Faire remonter automatiquement ces événements vers des salons Discord dédiés permet à toute l'équipe de rester informée en temps réel, sans jamais avoir besoin de se connecter directement au serveur.

Catégorie de logWebhook dédiéExemple d'événement
BannissementsWebhooks.bansUn joueur banni par un admin
ÉconomieWebhooks.economyPic d'argent anormal détecté
AdministrationWebhooks.adminCommande sensible utilisée

Piège fréquent

Envoyer des logs vers Discord sans file d'attente expose au code 429 (limite de 5 requêtes/2s par webhook dépassée) : les requêtes en trop sont simplement refusées, ce qui fait perdre silencieusement des logs potentiellement importants sans qu'aucune erreur ne remonte clairement.

Sortir les logs du serveur lui-même

Les techniques de logging vues en leçon 18 (console, fichiers) sont utiles pour déboguer, mais une équipe de modération ne surveille pas la console du serveur en continu. La quasi-totalité des communautés FiveM utilisent Discord comme plateforme de communication ; y faire remonter automatiquement les événements importants (bans, anomalies économiques) permet à toute l'équipe de rester informée en temps réel, sans avoir à se connecter au serveur lui-même.

Un webhook, la solution la plus simple qui suffit presque toujours

Un "bot Discord" complet nécessite une connexion permanente et une gestion d'état complexe. Pour simplement ENVOYER des messages formatés vers un salon Discord, un webhook suffit largement : c'est une simple URL à laquelle on envoie une requête HTTP, sans authentification complexe ni connexion permanente à maintenir. FiveM peut envoyer cette requête nativement avec PerformHttpRequest, sans dépendance externe.

Pourquoi respecter les limites imposées par Discord

Discord impose une limite stricte au nombre de requêtes qu'un même webhook peut recevoir en peu de temps. Dépasser cette limite ne provoque pas une erreur silencieuse ignorable : les requêtes en trop sont refusées (code 429), ce qui, sans précaution, ferait perdre des logs potentiellement importants. Une file d'attente qui espace les envois et retente automatiquement en cas de refus évite cette perte silencieuse d'information.

Séparer les catégories de logs, une question d'organisation

Envoyer tous les événements (bans, anomalies économiques, actions admin) vers un unique salon Discord le rend vite illisible. Utiliser un webhook distinct par catégorie permet à chaque salon Discord de rester focalisé sur un seul type d'information, facilitant grandement le travail de l'équipe de modération qui doit filtrer rapidement ce qui compte.

Ne jamais oublier la sécurité, même pour "juste afficher un statut"

Une route exposée, même pour une information qui semble anodine comme le nombre de joueurs connectés, doit rester protégée par les mêmes principes de permissions (ACE, vus en leçon 8) que n'importe quelle autre commande sensible — l'exposition d'informations, même mineures, doit toujours rester une décision consciente plutôt qu'un oubli.

Commandes & code

Intégration Discord : webhooks et bot de logs

Un webhook Discord suffit pour la plupart des logs serveur (pas besoin d'un bot complet avec gateway websocket).

lua
-- Envoi d'un embed Discord via webhook, depuis le serveur FiveM (PerformHttpRequest, natif, pas de dépendance externe)
local function SendDiscordLog(webhookUrl, title, description, color, fields)
    local embed = {
        {
            title = title,
            description = description,
            color = color,              -- couleur en décimal, ex: 15158332 pour un rouge
            fields = fields or {},
            footer = { text = 'Technologik RP' },
            timestamp = os.date('!%Y-%m-%dT%H:%M:%SZ'),   -- format ISO 8601 requis par l'API Discord
        }
    }

    PerformHttpRequest(webhookUrl, function(statusCode, response, headers)
        if statusCode ~= 204 and statusCode ~= 200 then
            print(('[Discord] Échec envoi webhook, code %s'):format(statusCode))
        end
    end, 'POST', json.encode({ embeds = embed }), { ['Content-Type'] = 'application/json' })
end
lua
-- Bibliothèque de logs centralisée avec plusieurs webhooks par catégorie (séparer bans, économie, admin...)
local Webhooks = {
    bans = GetConvar('discord_webhook_bans', ''),
    economy = GetConvar('discord_webhook_economy', ''),
    admin = GetConvar('discord_webhook_admin', ''),
}

local function LogBan(adminName, targetName, reason, duration)
    SendDiscordLog(Webhooks.bans, 'Bannissement', nil, 15158332, {
        { name = 'Admin', value = adminName, inline = true },
        { name = 'Joueur', value = targetName, inline = true },
        { name = 'Durée', value = duration, inline = true },
        { name = 'Raison', value = reason, inline = false },
    })
end

local function LogEconomyAnomaly(playerName, amount, reason)
    SendDiscordLog(Webhooks.economy, 'Anomalie économique', reason, 16776960, {
        { name = 'Joueur', value = playerName, inline = true },
        { name = 'Montant', value = tostring(amount), inline = true },
    })
end
lua
-- Rate-limiting des webhooks : Discord limite à 5 requêtes/2s par webhook, une file d'attente évite les 429
local WebhookQueue = {}
local isProcessingQueue = false

local function enqueueWebhook(url, payload)
    table.insert(WebhookQueue, { url = url, payload = payload })
    if not isProcessingQueue then
        isProcessingQueue = true
        Citizen.CreateThread(function()
            while #WebhookQueue > 0 do
                local item = table.remove(WebhookQueue, 1)
                PerformHttpRequest(item.url, function(code)
                    if code == 429 then
                        table.insert(WebhookQueue, 1, item)   -- remis en tête de file, sera retenté
                    end
                end, 'POST', item.payload, { ['Content-Type'] = 'application/json' })
                Citizen.Wait(500)   -- respecte la limite de 5 req/2s avec de la marge
            end
            isProcessingQueue = false
        end)
    end
end
lua
-- Vérification de statut serveur exposée à un bot Discord externe via une route HTTP simple (ox_lib ou txAdmin API)
-- côté serveur : endpoint minimal consultable par un bot pour afficher "X/64 joueurs en ligne" en topic du salon
RegisterCommand('discord_status_json', function()
    local players = {}
    for _, playerId in ipairs(GetPlayers()) do
        table.insert(players, GetPlayerName(playerId))
    end
    return json.encode({
        online = #players,
        max = GetConvarInt('sv_maxclients', 64),
        players = players,
    })
end, true)   -- true = commande restreinte, à sécuriser via un ACE approprié

Résumé

  • Un webhook Discord (via PerformHttpRequest) suffit pour la majorité des besoins de logs, sans dépendance à une lib de bot.
  • Séparer les webhooks par catégorie (bans, économie, admin) simplifie la modération et le filtrage côté Discord.
  • Discord limite à 5 requêtes/2s par webhook : une file d'attente avec retry sur 429 évite la perte silencieuse de logs.
  • Toute route exposée pour un bot externe doit rester protégée par ACE, même si elle ne fait que lire un statut.

Exercices pratiques

1 disponible
1

Mission : des logs Discord qui disparaissent en silence

Objectif : Diagnostiquer une perte de logs due à l'absence de file d'attente, la corriger, puis évaluer un choix d'organisation des webhooks.

Contexte

L'équipe de modération de technologik_rp remarque que certains bannissements n'apparaissent jamais dans le salon Discord dédié, alors que la commande de ban a bien fonctionné en jeu. La fonction SendDiscordLog de la leçon appelle directement PerformHttpRequest à chaque log, sans jamais passer par la file d'attente enqueueWebhook également définie dans la ressource mais jamais utilisée.

Résoudre l’exercice →