Discord-интеграция
Мосты SDLink / Mc2Discord / DCIntegration, вебхуки без мода-моста, синхронизация ролей в группы прав.
Как устроена интеграция#
NeoEssentials сам в Discord не ходит за ботом: нет своего токена и gateway. Есть четыре независимых куска:
- Мосты чата и событий — адаптеры SDLink, Mc2Discord, DCIntegration. Забирают уже связанный аккаунт у моста и шлют чат / join / leave / mute / AFK / advancement / личку в Discord.
- Вебхуки —
WebhookAdapter: обычный HTTPS POST на URL вебхука канала. Мост не нужен. Только Minecraft → Discord.
- Роли Discord → группа Minecraft —
discordrolesync.json и DCIntegration. Пишет во внутренний менеджер прав NeoEssentials.
- Вход в веб-панель по Discord —
discord_auth.json (roleMapping на ADMIN/MODERATOR/VIEWER). Это роли панели, не игрока на сервере. Команды панели (/dashboardregister discord, /linkaccount) на этой странице не разбираются.
Команд линковки у NeoEssentials нет. Игрок связывает Minecraft и Discord командами моста.
Мосты#
Порядок регистрации: SDLink → Mc2Discord → DCIntegration → Generic Webhook. Событие рассылается всем готовым адаптерам.
| Мост |
Чат и события |
Линк аккаунта |
Список ролей Discord |
| Simple Discord Link (SDLink) |
да |
да |
нет (публичное API не отдаёт) |
| Mc2Discord |
да |
да |
нет |
| DCIntegration |
да, в основном явный channelId |
да |
да |
| Generic Webhook |
да, если задан webhookUrl |
нет |
нет |
Чат настраивается внутри канала NeoEssentials: chat.channels.<имя>.discord.enabled / channelId / webhookUrl. Не-чат — сверху config.json в discordEventChannels. Команды каналов (/g, /staff, …) принадлежат системе каналов чата, не этой странице.
modules.discordIntegrationEnabled: false на старте пропускает ChatIntegrationManager.initialize() — не будет ни мостов, ни вебхуков, пока не включите флаг и не перезапустите.
Вебхуки#
URL берётся из того же webhookUrl рядом с channelId. Вебхук, добавленный через /neoe reload, начинает работать без рестарта — адаптер не кеширует URL. Поставить сам мод-мост по-прежнему можно только рестартом.
Обратного направления нет, ролей и линка нет, sendToChannel по snowflake id вебхук не умеет (дашбордный «test message» через него не уйдёт).
Двойная отправка#
У SDLink и Mc2Discord есть свой релей, параллельный NeoEssentials. На старте NeoEssentials читает их toml и для default-route глушит свои дубли, оставляя WARN. Явный channelId не глушится. Смена toml без рестарта детектор не перечитывает.
Роли → группы#
Файл discordrolesync.json, задача DiscordRoleSyncTask. Snowflake роли → имя группы из permissions.json. Несколько ролей — побеждает группа с большим priority. Только повышение. Только внутренний PermissionManager. Только DCIntegration как источник ролей. Обход по таймеру + сразу на join.
Примеры#
Чат в Discord без бота: webhook на канал global#
Прокинуть глобальный чат в Discord, не ставя SDLink / Mc2Discord / DCIntegration.
/neoe reload
В config.json → chat.channels.global.discord поставьте enabled: true и webhookUrl (Server Settings → Integrations → Webhooks → Copy Webhook URL). channelId бота можно оставить пустым. /neoe reload достаточно: WebhookAdapter перечитывает URL на каждое событие и регистрируется при старте даже без URL. В Discord сообщение придёт от имени игрока с аватаром mc-heads.net. Обратного моста Discord → Minecraft у вебхука нет.
Заходы и выходы в отдельный канал-лог#
Чат оставить в одном Discord-канале, join/leave отправить в другой — без привязки к каналу NeoEssentials.
/neoe reload
Секция discordEventChannels в config.json: у join и leave поставьте enabled: true и либо channelId (нужен мост-бот), либо webhookUrl, либо оба — тогда сработают независимо. Ключи событий: join, leave, mute, afk, advancement, privateMessage. Пустой channelId при enabled: false означает «пусть мост шлёт в свой родной канал». После reload вебхук подхватывается сразу; смена самого моста (поставили SDLink) требует рестарта — адаптеры создаются только в ChatIntegrationManager.initialize() на старте.
Роль Discord → группа permissions.json#
Игрок с ролью Boosters на Discord должен попасть в группу vip внутреннего менеджера прав.
/neoe reload
В discordrolesync.json → discordRoleSync включите enabled, пропишите snowflake роли в roleGroupMap на имя группы из permissions.json. Нужны DCIntegration (только он отдаёт список ролей) и внутренний PermissionManager: при LuckPerms/FTB Ranks PermissionAPI.isUsingExternal() — синк молча выходит. Игрок должен быть связан с Discord командами DCIntegration, не NeoEssentials. На join синк идёт сразу; периодический обход (intervalSeconds, по умолчанию 300) стартует только если флаг был true в момент ServerStartedEvent. Если включили впервые уже на работающем сервере — перезайдите персонажем или перезапустите сервер, иначе таймер не поднимется.
Убрать удвоенный префикс и двойные join-сообщения#
В Discord [Owner] [Owner] Name или два «joined» подряд: родной релей моста плюс NeoEssentials.
/neoe reload
На старте адаптер читает config/simple-discord-link/simple-discord-link.toml или config/mc2discord.toml и пишет WARN в лог (и в GET /api/discord/status → nativeRelayWarnings): какие события он сам слать не будет. Явный channelId в chat.channels.*.discord или discordEventChannels всё равно уходит — считается, что это другой канал. Чтобы единственным источником был NeoEssentials, выключите родной релей моста (chat.playerMessages / playerJoin / … у SDLink; подписки канала у Mc2Discord) и перезапустите. /neoe reload конфиг моста заново не парсит.
Тонкие места#
Вебхук тоже требует `modules.discordIntegrationEnabled`#
Хотя вебхуку не нужен мод-мост, WebhookAdapter попадает в список только из ChatIntegrationManager.initialize(), а тот вызывается при старте лишь если modules.discordIntegrationEnabled в config.json истинен. При false не будет ни ботов, ни вебхуков. Сам адаптер при пустом URL просто idle и начнёт постить после /neoe reload, когда URL появится — но только если его уже зарегистрировали на старте.
Что делать: Оставляйте discordIntegrationEnabled: true, даже если стоите только на webhookUrl. Смену модуля — рестартом.
Два разных файла синхронизации ролей#
discordrolesync.json — DiscordRoleSyncTask: Discord role id → имя группы permissions.json, побеждает группа с большим priority, никогда не понижает, роли читает только DCIntegration.
discord_auth.json → permissionSync — DiscordPermissionSync на join (задержка 1 с): те же роли через ChatIntegrationManager.findDiscordRoleIds (опять DCIntegration), но маппинг permissionMappings в коде берёт первый элемент списка как имя группы, остальные элементы игнорируются. Комментарий файла обещает узлы вроде neoessentials.* / neoessentials.kick. Поставка как раз кладёт узлы в этот список — setGroup("neoessentials.*") запишет несуществующую группу.
roleMapping / roleHierarchy в том же файле — это роли веб-панели (ADMIN/MODERATOR/VIEWER), не Minecraft.
Что делать: Группы игроков на сервере настраивайте в discordrolesync.json. permissionMappings не используйте как список узлов — либо выключите permissionSync.enabled, либо поставьте первым элементом реальное имя группы. Два синка сразу не включайте: оба пишут user.setGroup на join.
Список ролей есть только у DCIntegration#
SDLinkAdapter.getDiscordRoleIds всегда возвращает пустой список (публичное API SDLink не отдаёт роли). Mc2Discord использует default адаптера — тоже пусто. ChatIntegrationManager.findDiscordRoleIds берёт первый непустой ответ. Без DCIntegration и role-sync, и permissionSync на join не увидят ни одной роли (линк аккаунта при этом SDLink/Mc2Discord отдать могут).
Что делать: Синхронизация ролей → группы: ставьте DCIntegration и линкуйте аккаунт его командой. Для одного только чата достаточно SDLink, Mc2Discord или вебхука.
Синк только повышает группу и не пишет во внешний плагин#
DiscordRoleSyncTask выходит, если PermissionAPI.isUsingExternal(). Игрока без совпавшей роли не трогает — админская группа не сбросится в default, когда буст в Discord кончится. Интервал таймера фиксируется в start() на старте сервера: правка intervalSeconds подхватывается после рестарта, карта ролей из файла читается каждый проход (после /neoe reload cache конфигов сброшен).
Что делать: Понижение делайте вручную. С LuckPerms синхронизируйте роли их Discord-плагином, не этим файлом. Смену периода обхода — рестартом.
`channelId` и `webhookUrl` на одном событии#
Ботовые адаптеры смотрят channelId, вебхук — webhookUrl. Если заполнены оба, уйдут оба сообщения. Явный channelId не подавляется детектором native relay: детектор глушит только default-route. discord.enabled: false у канала чата выключает и вебхук этого канала (resolveChatWebhook проверяет enabled).
Что делать: Для одного направления оставьте одно поле. Чтобы не дублировать join с родным релеем SDLink, либо положитесь на WARN и default-route, либо шлите join только явным discordEventChannels.join.channelId в канал, куда SDLink сам не пишет.
Эмбеды SDLink и обрезка текста#
discordEmbedTemplate (и вложенные join/leave/…) применяются к явной отправке в channelId и только адаптером SDLink. Mc2Discord и DCIntegration шлют обычный текст. Любой пользовательский текст во все три моста обрезается до лимита Discord (~2000) до отправки. Вебхук чата/advancement подставляет username и avatar игрока; join/leave/mute/afk идут от имени Server без аватара.
Что делать: Цветные embed-join — SDLink + непустой discordEventChannels.join.channelId. Вебхук так не умеет.
Команд NeoEssentials на линковку нет#
В инвентаре мода нет /discord, /link, /syncperms. Линк делается командами моста. /dashboardregister discord принадлежит веб-панели и только создаёт аккаунт панели по уже существующему линку. Узлы neoessentials.discord.syncperms в комментарии discord_auth.json — примеры того, что можно было бы выдать, в PermissionRegistry их нет, отдельной команды нет.
Что делать: Не ищите синк в /neoe. Связку аккаунтов настраивайте в SDLink / Mc2Discord / DCIntegration, затем файлы NeoEssentials.
Известные проблемы#
`permissionSync.permissionMappings` не выдаёт узлы прав#
DiscordPermissionSync.mapDiscordRoleToPermissionGroup берёт permissions.get(0) и передаёт в user.setGroup. Существование группы не проверяется. Поставляемый discord_auth.json начинает списки с neoessentials.*, neoessentials.dashboard.temppass и т. д. — это не имена групп. Плюс reloadConfig() ниоткуда не вызывается: после правки файла синглтон продолжает старый маппинг до рестарта, хотя /neoe reload сбрасывает JSON-кеш.
Что делать: Выключите permissionSync.enabled и пользуйтесь discordrolesync.json. Если оставляете этот путь — первым элементом списка должно быть имя группы из permissions.json, и после правки нужен рестарт, не только /neoe reload.
Включили `discordRoleSync.enabled` на уже запущенном сервере — периодический обход молчит#
DiscordRoleSyncTask.start() вызывается из ServerStartedEvent и сразу выходит, если enabled тогда был false. Join-обработчик читает флаг каждый раз и после /neoe reload уже работает. ScheduledExecutorService при этом никто не поднимает — смена роли без перезахода подтянется только после рестарта (или не подтянется, пока игрок онлайн).
Что делать: Первое включение role-sync — рестартом. Дальше достаточно /neoe reload для карты ролей; для смены intervalSeconds снова рестарт.
Детектор native relay не обновляется с `/neoe reload`#
Разбор simple-discord-link.toml / mc2discord.toml делается в initialize() адаптера на старте. Правка родного конфига моста или переключение его native-ключей без рестарта оставит старый набор подавлений: либо снова пойдут дубли, либо NeoEssentials продолжит молчать, хотя native уже выключен.
Что делать: После правок конфига моста перезапускайте сервер. Смотрите WARN на старте и nativeRelayWarnings в статусе панели.
Родной релей моста по-прежнему может удвоить префикс#
Сообщения, которые шлёт сам SDLink из chat.playerMessages, читают Player.getDisplayName() (там уже префикс scoreboard-команды таблиста) и отдельно клеят ранг из LuckPerms/FTB Ranks. Это закрытый код моста. Сообщения адаптеров NeoEssentials с build 68 берут префикс из PermissionAPI и не удваиваются. Детектор как раз глушит default-route NeoEssentials, когда native включён — тогда в Discord остаётся именно удваивающий native.
Что делать: Выключите native-релей моста для этого события и перезапустите, чтобы в канал шёл только адаптер NeoEssentials. tablist.json → nametagSettings.enabled: false убирает префикс с таблиста/над головой целиком — это не точечный фикс Discord.
Конфигурация#
discord_auth.json#
discordrolesync.json#