MAATRIX GAMES / Блог / Discord-интеграция для RP-сервера: тикеты и whitelist

Discord-интеграция для RP-сервера: тикеты и whitelist

MAATRIX GAMES

Половина RP-админов ведёт заявки на whitelist в личных сообщениях, а роли на Discord-сервере и права в игре живут разными жизнями — модератор ушёл из стаффа два месяца назад, а роль в Discord так и висит. Тикеты поддержки тонут в общем чате вперемешку с «когда вайп», а о рестарте сервера половина комьюнити узнаёт только по факту дисконнекта. Разберём, как собрать вокруг RP-сервера на FiveM/QBCore (принципы применимы и к ESX, и, частично, к RP-серверам на других движках) рабочий Discord-контур: тикеты, автоматизацию whitelist, синхронизацию ролей через discord_perms, уведомления о рестартах и логирование банов с жалобами.

Что нужно от бота до старта

Прежде чем городить логику, закрой два технических момента, без которых половина ниже описанного просто не заработает.

Privileged Gateway Intents. Чтобы бот вообще видел роли участников гильдии, в Discord Developer Portal на вкладке бота включи Server Members Intent — без него guild.members.fetch() в discord.js вернёт пусто или урезанный список, и синхронизация ролей с игрой не будет знать, какие роли у игрока есть.

Права бота, а не роль Administrator. Хватает точечных прав: Manage Channels (тикет-каналы), Manage Roles (роль «Whitelisted» и роли стаффа), Send Messages, Embed Links, Use Application Commands. Нюанс: бот выдаёт и снимает только те роли, что стоят ниже его собственной в иерархии сервера — если роль бота ниже «Модератора», присвоить её он физически не сможет.

Структура каналов. Удобная база — отдельная категория «Тикеты» (создаётся динамически под каждое обращение), канал #заявки-whitelist для формы, #restart-log и #ban-log как read-only ленты для стаффа, #жалобы как точка входа под модалку жалобы.

Если бота ещё нет вообще — сначала пройди базовую установку Discord-бота: токен, discord.js, автозапуск через pm2/systemd. Здесь исходим из того, что бот уже крутится 24/7, и говорим только про RP-специфичную начинку.

Тикеты поддержки: готовый бот или своя логика

Два рабочих пути, и оба закрывают задачу «жалоба/вопрос не должны тонуть в общем чате».

Готовые боты (Ticket Tool, Tickets и похожие) — быстрее в развёртывании: приглашаешь бота, настраиваешь категорию и роль стаффа его собственными командами, получаешь кнопку «Открыть тикет» за 10 минут. Минус — зависимость от чужого аптайма и лимитов бесплатного тарифа (обычно ограничение на число одновременных тикетов).

Своя логика на discord.js — больше контроля, без внешних зависимостей. Кнопка в #поддержка создаёт приватный канал в категории «Тикеты» с точечными правами: автор тикета и роль стаффа видят канал, @everyone — нет.

const { PermissionFlagsBits, ChannelType } = require('discord.js');

async function createTicket(interaction) {
  const guild = interaction.guild;
  const staffRoleId = process.env.STAFF_ROLE_ID;

  const channel = await guild.channels.create({
    name: `ticket-${interaction.user.username}`,
    type: ChannelType.GuildText,
    parent: process.env.TICKETS_CATEGORY_ID,
    permissionOverwrites: [
      { id: guild.id, deny: [PermissionFlagsBits.ViewChannel] },
      { id: interaction.user.id, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages] },
      { id: staffRoleId, allow: [PermissionFlagsBits.ViewChannel, PermissionFlagsBits.SendMessages] },
    ],
  });

  await channel.send({
    content: `<@${interaction.user.id}> опишите вопрос, стафф подключится в ближайшее время.`,
  });
  await interaction.reply({ content: `Тикет создан: ${channel}`, ephemeral: true });
}

Кнопку «Закрыть тикет» вешай отдельным обработчиком, который архивирует переписку (discord-html-transcripts собирает HTML-транскрипт перед удалением канала) и постит его в архивный канал — иначе история обращений теряется вместе с каналом. Та же логика создания канала пригодится и для жалоб — только меняется набор полей во вступительном сообщении.

Нужен игровой сервер?

MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.

Создать сервер

Whitelist-заявки через модальную форму

Ручная whitelist-модерация в личных сообщениях — источник хаоса: заявки теряются, дубли, невозможно быстро посмотреть историю решений. Модальная форма (Modal) в discord.js закрывает это почти полностью.

Схема: кнопка «Подать заявку» в #заявки-whitelist открывает модальное окно с полями — игровой ник, желаемый персонаж/бэкграund (коротко), подтверждение прочтения правил. После сабмита бот собирает embed и постит его в приватный канал стаффа с кнопками «Одобрить»/«Отклонить».

const { ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder } = require('discord.js');

const modal = new ModalBuilder()
  .setCustomId('whitelistForm')
  .setTitle('Заявка на whitelist');

const nickInput = new TextInputBuilder()
  .setCustomId('nickname')
  .setLabel('Игровой ник')
  .setStyle(TextInputStyle.Short)
  .setRequired(true);

const backstoryInput = new TextInputBuilder()
  .setCustomId('backstory')
  .setLabel('Кратко о персонаже')
  .setStyle(TextInputStyle.Paragraph)
  .setRequired(true);

modal.addComponents(
  new ActionRowBuilder().addComponents(nickInput),
  new ActionRowBuilder().addComponents(backstoryInput),
);

При нажатии «Одобрить» бот делает две вещи: выдаёт заявителю роль «Whitelisted» в Discord (member.roles.add(roleId)) и добавляет его в whitelist на стороне сервера. Второй шаг реализуется по-разному — RCON-командой, если whitelist файловый/командный, либо записью в MySQL-таблицу, которую читает ресурс QBCore/ESX при коннекте (playerConnecting). Общий принцип whitelist по ID, а не по нику, разобран в статье whitelist — как настроить доступ на сервер.

Игровой идентификатор заявитель обычно не знает наизусть, поэтому в форму берут Discord ID (interaction.user.id), а привязка к игровому аккаунту происходит на первом коннекте — сервер сверяет identifier.discord игрока с одобренными заявками и только тогда пускает в мир.

Синхронизация ролей: discord_perms в FiveM/QBCore

FiveM умеет опознавать Discord-аккаунт игрока как отдельный тип идентификатора — identifier.discord:<id> — наравне со steam, license, ip. Он появляется в списке идентификаторов автоматически, если у игрока запущен Discord (через Rich Presence/IPC) в момент подключения к серверу. На этом идентификаторе строится вся система прав FiveM — стандартные ACE-команды:

add_ace identifier.discord:834728948573841234 group.admin allow
add_principal identifier.discord:834728948573841234 group.admin

Проблема в том, что identifier.discord даёт только сырой ID аккаунта, а не роли игрока на твоём Discord-сервере — ACE-система FiveM сама их не знает. Эту связку закрывает паттерн, который в комьюнити называют discord_perms: ресурс с ботом (нужен тот самый Server Members Intent), который при подключении игрока или по расписанию запрашивает его роли в гильдии через Discord API, сверяет их с конфигом маппинга «роль Discord → ACE-группа» и выполняет add_principal/remove_principal за игрока.

Форков этого паттерна на GitHub несколько, и синтаксис конфигурации отличается от реализации к реализации — перед установкой сверяйся с README конкретного ресурса, а не полагайся на универсальный пример. Общая логика везде одна:

-- псевдоконфиг маппинга (конкретные ключи зависят от выбранного ресурса)
RoleMapping = {
  ['Owner Discord Role ID']     = 'god',
  ['Admin Discord Role ID']     = 'admin',
  ['Moderator Discord Role ID'] = 'moderator',
  ['Whitelisted Discord Role']  = 'whitelisted',
}

Практический плюс такой синхронизации — снимать роль в Discord теперь достаточно, чтобы человек мгновенно потерял и права в игре, без похода в консоль сервера отдельно. Для команд, где стафф меняется часто, это закрывает классическую дыру «уволили модератора, а бан-права на сервере забыли отозвать». Если у тебя ESX или QBCore и ты ещё не определился с фреймворком под RP-ядро — сравнение архитектур и того, как в каждом устроена система прав, есть в статье ESX и QBCore: сравнение фреймворков.

Уведомления о рестартах в канал

Игрок, которого просто выкинуло с сервера без предупреждения, обычно решает, что сервер упал, а не что это плановый рестарт — и это лишние жалобы в поддержку на пустом месте.

Если сервер на FXServer, у тебя уже есть встроенный инструмент — txAdmin (идёт в комплекте, открывается на порту 40120 по умолчанию). В веб-панели, раздел настроек Discord, можно указать webhook канала и включить нужные события — рестарт сервера, действия модерации — без единой строчки кода. Это самый быстрый путь, если отдельного бота нет.

Второй путь — ручной, через cron + webhook, удобен для серии предупреждений (за 15, 5 и 1 минуту) с объявлением в чате через RCON:

#!/bin/bash
# warn-restart.sh — вызывается cron-ом за 15/5/1 минуту до рестарта
MSG="$1"
curl -s -X POST -H "Content-Type: application/json" \
  -d "{\"content\":\"🔄 Рестарт сервера через ${MSG}\"}" \
  https://discord.com/api/webhooks/XXXXXXXX/YYYYYYYYYY

mcrcon -H 127.0.0.1 -P 30120 -p "$RCON_PASSWORD" "say Рестарт сервера через ${MSG}, сохраните прогресс"

Про сам cron-график для регулярных рестартов и его типичные грабли (например, рестарт посреди активного RP-сюжета) — в статье автоматический рестарт сервера по расписанию: здесь просто добавляешь вызов вебхука в те же cron-строки.

Логирование банов и жалоб

Два потока, которые часто путают, хотя решаются похоже: логи банов — это лента для стаффа «что происходит», а жалобы — входящий канал от игроков.

Баны и кики. На txAdmin банов через встроенную панель автоматически хватает в тот же webhook, что настроен для рестартов (отдельным тумблером). Если баны идут через кастомный ресурс на QBCore/ESX, добавь серверный ивент, который постит embed в #ban-log:

-- server-side, вызывается из твоей команды /ban
local function logBanToDiscord(admin, target, reason, duration)
  local embed = {
    { title = "Игрок забанен", color = 15158332, fields = {
        { name = "Модератор", value = admin, inline = true },
        { name = "Игрок", value = target, inline = true },
        { name = "Причина", value = reason, inline = false },
        { name = "Срок", value = duration, inline = true },
    }}
  }
  PerformHttpRequest(WEBHOOK_URL, function() end, 'POST',
    json.encode({ embeds = embed }), { ['Content-Type'] = 'application/json' })
end

Жалобы. Отдельная кнопка/модалка в стиле тикетов, но с полями под конкретику: на кого жалоба, время инцидента в игре, ссылка на пруф, свидетели. Заявка создаёт приватный тикет-канал в категории «Жалобы», видимый только автору и роли модерации — жалующийся не боится огласки, а стафф разбирает по одной. Решение (бан, предупреждение, отказ) стоит фиксировать текстом в тикете перед закрытием — это и есть история модерации на случай споров задним числом.

Нужен игровой сервер?

MAATRIX GAMES — 45+ игр, деплой за минуты, DDoS-защита, автобэкапы. Тарифы от 399 ₽/мес.

Создать сервер

Частые вопросы

Стоит ли писать всё с нуля или искать готового RP-бота под FiveM/QBCore?

Готовые боты существуют, но перед установкой смотри исходный код или отзывы комьюнити — бот получает доступ к ролям и часто к RCON-паролю, а это чувствительная точка входа. Своя логика на discord.js из этой статьи занимает пару вечеров и полностью под твоим контролем.

Что если у меня несколько RP-серверов (тестовый и боевой) на одном Discord?

Держи отдельный конфиг маппинга ролей и RCON-данных под каждый сервер и добавляй параметр сервера в команды бота — тот же принцип, что и для управления несколькими игровыми серверами одним ботом.

Что будет, если бот или Discord API временно недоступны — стафф потеряет права в игре?

Нет — ACE-права, выданные ранее add_principal, остаются в силе до явного remove_principal и не завязаны на постоянную связь с ботом. Недоступность бота остановит только новые выдачи/отзывы прав.

Безопасно ли давать боту право Manage Roles и доступ к RCON одновременно?

Технически да, но храни RCON-пароль и токен бота в .env, ограничь роль бота в иерархии (выше управляемых ролей, но не выше владельца) и никогда не выдавай боту право Administrator целиком — точечных прав из первого раздела достаточно.