Denizen и Discord: dDiscordBot
Введение
dDiscordBot — это дополнительный плагин к Denizen, который даёт команды, события и теги для работы с API Discord. По сути, вы можете прямо в Denizen создать полноценного Discord-бота!
Что он умеет?
Для Discord API существует множество библиотек на самых разных языках. Например, вы наверняка слышали о Discord.Net или JDA (обе библиотеки используются и в проектах самого Denizen). Эти библиотеки стремятся покрыть все аспекты API Discord.
Denizen создавался для Minecraft-серверов. Соответственно, dDiscordBot предоставляет инструменты, которые нужны именно Minecraft-серверу для взаимодействия с Discord. Тем не менее, вот что он умеет:
Отправлять, получать и отвечать на сообщения
Прикреплять файлы, embed'ы и кнопки
Управлять slash-командами и отвечать на них
Управлять ролями
…и многое другое.
Для чего он нужен?
У многих Minecraft-серверов есть Discord-сообщества. Они дают социальное пространство за пределами самой игры. Пользователям Discord может хотеться взаимодействовать с игроками на сервере, и наоборот.
Основные сценарии использования — линковщик аккаунтов, который связывает пользователя Discord с его игроком на сервере, и чат-мост между Discord и Minecraft. Также можно ретранслировать объявления, проводить кросс-платформенные ивенты или позволять пользователям Discord запрашивать данные с сервера.
Создание бота
В сети достаточно туториалов по созданию бот-аккаунта — например, вот этот.
На странице бота обязательно включите Server Members Intent и Message Content Intent.
Когда будете генерировать ссылку-приглашение для бота, перейдите на вкладку «OAuth2» в панели разработчика и поставьте галочку bot. Скорее всего, вы также захотите поставить галочку applications.commands, если планируете использовать slash-команды.
Альтернативно можно просто использовать такую ссылку-приглашение: https://discord.com/oauth2/authorize?scope=bot%20applications.commands&permissions=0&client_id=1234, подставив вместо 1234 в конце ID вашего приложения бота.
В большинстве случаев лучше не выдавать боту права на странице «OAuth2» — иначе в настройках ролей вашего сервера будет беспорядок.
Установка на сервер
Когда мы говорим, что dDiscordBot — это аддон-плагин, это значит, что он не входит в основной jar-файл Denizen — у него собственный jar, который нужно скачать и положить в папку plugins. Скачать можно здесь.
Если вы устанавливаете dDiscordBot впервые, для его загрузки нужно перезапустить сервер. После перезапуска на сервере станут доступны разнообразные Discord-команды, теги и события.
Вход
После того как вы создали бота на странице приложений Discord, добавили его в своё Discord-сообщество и установили dDiscordBot на Minecraft-сервер, следующий шаг — войти в этого Discord-бота с вашего Minecraft-сервера.
Чтобы войти от имени бота, используйте команду discordconnect.
Из соображений безопасности токен нужно указать через SecretTag — для этого положите токен в файл plugins/Denizen/secrets.secret. Просто добавьте туда ключ вида discord_bot_token: 123.abc (и подставьте вместо 123.abc токен со страницы приложения Discord, который вы получили выше).
Команда discordconnect принимает аргумент id. Это может быть любая удобная вам метка (например, mybot, ticketbot или relay) — она нужна, чтобы различать ботов, если на сервере одновременно работает несколько. Почти все Discord-команды требуют этот аргумент, поэтому просто следите, чтобы везде использовать одну и ту же метку.
Учтите, что подключаться и отключаться можно в любой момент, но обычно достаточно одного подключения — как правило, его делают сразу после старта сервера через событие server start. Кроме того, не забудьте ~wait for для этой команды, как и для всех остальных Discord-команд.
connect_to_discord:
type: world
events:
after server start:
- ~discordconnect id:mybot token:<secret[discord_bot_token]>
Отправка сообщения
Когда вы вошли, можно отправить первое сообщение. У команды discordmessage есть аргумент channel — это Discord-канал, в который бот должен отправить сообщение. Само собой, у бота должны быть права на просмотр и отправку сообщений в этом канале. Можно использовать полноценный объект канала там, где у вас есть ссылка на него, или просто внутренний ID канала в Discord. Если не знаете, как посмотреть ID в Discord, — вот инструкция.
Если вы планируете часто использовать ID канала или сервера, лучше сохранить его в data-скрипте или где-нибудь зафлаговать. Например, прямо в игре можно выполнить /ex flag server discord_botspam:<discord[mybot].group[Denizen].channel[bot-spam]>, подставив нужные имена — и обязательно загляните в отладочный вывод, чтобы убедиться, что всё подставилось правильно. Можно проверить, /ex narrate <server.flag[discord_botspam].name> — выведет имя сохранённого канала.
Вот как отправка сообщения в канал выглядит в простом task-скрипте:
send_a_message:
type: task
script:
- ~discordmessage id:mybot channel:<server.flag[discord_botspam]> "Hello, world!"
Сообщения покрасивее: Embed'ы
Если вы когда-нибудь общались с ботом в Discord (а раз вы дошли до создания своего — наверняка да), то знаете, что боты часто отвечают не голым текстом, а специально оформленными блоками. Эти блоки называются embed'ами.

В Denizen для этого используются объекты DiscordEmbedTag. Они отправляются той же командой discordmessage, только вместо текста сообщения подставляется объект embed'а.
DiscordEmbedTag — это, по сути, обёртка над MapTag с данными. В простейшем случае его можно создать через тег discord_embed и заполнить данными через DiscordEmbedTag.with[key].as[value] или DiscordEmbedTag.with_map.
У DiscordEmbedTag длинный список поддерживаемых ключей, за подробностями загляните в документацию по тегам этого объекта. Чаще всего используются title и description.
Простой embed можно полностью описать одной строкой через with_map и map-синтаксис. Более сложные данные, особенно с тегами, лучше задавать через with[...].as[...], чтобы избежать кривого парсинга, или через команду definemap.
Пример простого inline-использования:
send_an_embed:
type: task
script:
- define embed <discord_embed.with_map[title=Example Bot;description=Wow! This bot sure is a bot;timestamp=<util.time_now>;color=#00FFFF]>
- ~discordmessage id:mybot channel:<server.flag[discord_botspam]> <[embed]>
Имейте в виду, что в любом сообщении или embed'е для перевода строки можно использовать тег <n>.
Автоматическая отправка сообщений
Давайте это автоматизируем! Используя событие discord message received, можно ловить сообщения от пользователей Discord и выполнять команды в зависимости от их содержимого. Можно даже ответить прямо в том же канале, откуда пришло сообщение! Например, если вы хотите отвечать пользователю, который написал «ping», это делается так:
ping_pong:
type: world
events:
after discord message received:
- if <context.new_message.text.contains_text[ping]>:
- ~discordmessage id:mybot reply:<context.new_message> Pong!
# или: - ~discordmessage id:mybot channel:<context.new_message.channel> Pong!

Пример: чат-мост
Теперь у вас есть базовые инструменты, чтобы сделать чат-мост. Простой чат-мост состоит из двух частей:
Когда игрок Minecraft пишет в чат — отправлять сообщение в Discord-канал.
Когда пользователь Discord пишет в этот канал — рассылать его сообщение игрокам на сервере.
Начнём с первой части. Можно использовать событие player chats. Дальше достаточно вызвать команду discordmessage, как в первом примере. Стоит также добавить в сообщение ник игрока Minecraft, иначе в Discord не поймут, от кого оно.
Учтите, что форматирование Discord использует Markdown — теги Minecraft-форматирования Denizen вроде <bold> здесь работать не будут.
chat_bridge:
type: world
events:
after player chats:
- define message "**<player.name>**: <context.message>"
- ~discordmessage id:mybot channel:<server.flag[discord_chatrelay]> <[message]>
Отлично! Теперь вторая часть. Можно использовать то же событие discord message received, что и в примере выше, но теперь нужно ретранслировать только сообщения из конкретного канала. У этого события есть переключатель channel — то, что нужно. Поскольку сообщение будет показано в Minecraft, здесь уже используем Minecraft-теги форматирования, а не Discord-Markdown.
Учтите, что полученный объект DiscordMessageTag хранит не только текст: из него можно получить автора, канал, ID и многое другое.
chat_bridge:
type: world
events:
# Замените '12345' на скопированный raw-ID канала.
# К сожалению, в строках меток событий теги вроде server.flag пока не работают.
after discord message received channel:12345:
# напр.: [Discord] <acikek> Hello!
- announce "[<blue>Discord<&r>] <<><context.new_message.author.name><>> <context.new_message.text>"
Slash-команды
Slash-команды — это новый способ взаимодействовать с ботом в Discord по запросу. Они встроены в клиент, так что посмотреть подсказку по команде можно без внешних источников. Кроме того, они появляются в списке, когда вы начинаете сообщение с / — попробуйте сами!

С помощью dDiscordBot вы можете создавать свои slash-команды. Они относятся к набору фич под названием Interactions, вместе с кнопками и меню выбора.
Slash-команду достаточно создать один раз. Создание slash-команды с тем же именем обновит существующую.
Когда пользователь вызывает slash-команду, ответ нужно дать в течение всего 5 секунд. Но это не значит, что вы обязаны сразу отправить сообщение — если нужно больше времени, можно отложить (defer) запрос, то есть подтвердить его, и ответить позже.
Подробнее об ограничениях slash-команд можно почитать здесь.
Пример: команда «последний вход»
Сделаем slash-команду, которая показывает, когда игрок последний раз заходил. Если игрок сейчас онлайн, скажем об этом. Сначала нужно создать команду. Это делается командой discordcommand с аргументом create. Мы хотим принимать от пользователя имя игрока — это называется option, его нужно прикрепить к команде при создании.
При создании slash-команды можно указать сервер, на котором она будет доступна, через аргумент group. Это очень удобно для тестирования, даже если в итоге команду хочется сделать глобальной. Регистрация глобальной команды может занять до часа!
Аргумент options — это map из map'ов, значения в которых задаются в определённом формате. Формат можно посмотреть на мета-странице. Для этого рекомендуется использовать команду definemap.
Аргумент name обязателен, description — нет, но он полезен для пользователей. Соберём всё в task-скрипте:
create_lastlogin:
type: task
script:
- definemap options:
1:
type: string
name: player
description: The Minecraft player's name
required: true
- ~discordcommand id:mybot create name:lastlogin "description:Displays a player's last login time." "group:<discord[mybot].group[My server]>" options:<[options]>
После создания команды можно использовать событие discord slash command, чтобы ловить вызовы. Обязательно используйте переключатель name с именем команды.
Использование slash-команд, кнопок и меню выбора называется interaction — это те самые вещи, которые мы обязаны подтверждать, как говорилось выше. С этим справляется команда discordinteraction с обязательным аргументом interaction. Обратите внимание: этой команде не нужен аргумент id. У всех трёх соответствующих событий есть тег <context.interaction>, который сам по себе содержит ссылку на бота.
Хорошая практика — откладывать (defer) ответ, даже если interaction не займёт много времени. Используйте инструкцию defer для подтверждения и reply — для ответа сообщением. Пока что просто проверим работу на «Hello, world»:
lastlogin:
type: world
events:
on discord slash command name:lastlogin:
- ~discordinteraction defer interaction:<context.interaction>
- ~discordinteraction reply interaction:<context.interaction> "Hello, world!"

Наша команда должна работать с переданным option. Поскольку мы выставили player в required, стандартный Discord-клиент не даст пользователю вызвать slash-команду без этого аргумента.
Тег <context.options> у события discord slash command возвращает MapTag из имён option'ов и их значений.
Имя игрока, которое ввёл пользователь, можно получить через <context.options.get[name]> и подставить в <server.match_offline_player[...]>, чтобы получить реальный объект игрока по этому имени.
Как мы учим на странице «Частые ошибки: не доверяйте игрокам», никогда нельзя доверять пользовательскому вводу. Даже в этой простой ситуации пользователь Discord может: ввести имя несуществующего игрока, ввести строку, которая вообще не является ником, ввести пустое значение, или как-то сжульничать и обойти список required-опций, и в результате прийти вообще без player'а. Поэтому тщательно проверяйте каждую часть ввода и обрабатывайте ошибочные случаи простым сообщением-отказом.
Дальше — нужно проверить, онлайн игрок или нет; это делается через тег PlayerTag.is_online. Если онлайн — просто сообщаем об этом. Если нет — используем тег PlayerTag.last_played_time и форматируем полученный TimeTag. Можно использовать тег TimeTag.format_discord, чтобы аккуратно показать время в Discord-формате — или собрать свой формат через TimeTag.format[...].
Обязательно укажите имя игрока в сообщении! <server.match_offline_player[...]> возвращает лучшее совпадение по вводу, то есть если пользователь ошибся с ником, найденный игрок не всегда будет тем, кого он имел в виду.
Итоговый скрипт:
lastlogin:
type: world
events:
on discord slash command name:lastlogin:
- ~discordinteraction defer interaction:<context.interaction>
# Пустой ввод в match_offline_player гарантированно вернёт null.
- define player <server.match_offline_player[<context.options.get[player].if_null[<empty>]>].if_null[null]>
- if <[player]> == null:
- ~discordinteraction reply interaction:<context.interaction> "That name is invalid, or that player has never joined!"
- stop
- if <[player].is_online>:
- ~discordinteraction reply interaction:<context.interaction> "**<[player].name>** is online!"
- else:
- define message "**<[player].name>** was last seen: <[player].last_played_time.format_discord>"
- ~discordinteraction reply interaction:<context.interaction> <[message]>

(В этой гифке использовался формат <[player].last_played_time.format[LLLL dd, yyyy 'at' hh:mm a]> вместо тега format_discord)
Кликабельные штуки: компоненты
Остаток набора фич Interactions — кнопки и меню выбора — относится к собственной категории: компоненты (components). Компоненты можно прикреплять и к ответам на interactions, и к обычным сообщениям (через аргумент rows), и при использовании они возвращают interaction.
Объект DiscordButtonTag можно создать через <discord_button>. Свойства задаются через теги в стиле with, как и в DiscordEmbedTag. DiscordSelectionTag устроен так же.
Поддерживаемые свойства кнопки можно посмотреть здесь, а меню выбора — здесь.
Аргумент rows — это список списков. Внешние списки — это ряды, а вложенные — компоненты в каждом ряду (по сути, как колонки в сетке). Можно использовать definemap, чтобы описать эту вложенную структуру проще. Для одной кнопки можно просто передать её как значение, без оборачивания в список. Аргумент rows есть и у discordinteraction, и у discordmessage. Компоненты внутри рядов можно свободно миксовать, как вам удобно. Но само сообщение всё равно должно быть!
Что попробовать
На этой странице мы разобрали два небольших проекта и основы message-компонентов. Если хочется идей, загляните в раздел «Для чего он нужен?». Если ищете челлендж, можно попробовать:
Смешивать компоненты в одном сообщении
Отвечать на interactions приватно (подсказка: документация!)
Сделать команду «инфо об игроке»
Фантазируйте! Discord — это ещё одна платформа, полная своих концепций и возможностей, на которых можно развернуться.
Частые проблемы: когда что-то пошло не так
Если при тестировании бота что-то пошло не так, вот несколько частых проблем и способы их решить.
Прежде всего, если что-то не работает — убедитесь, что на странице бота в Discord включён
Server Members Intent. Без него ломается очень многое.Убедитесь, что у бота есть права на то, что вы пытаетесь делать! Бот должен иметь возможность видеть нужный канал, читать в нём сообщения и отправлять туда сообщения. На этапе первого тестирования лучше давать больше прав, чем меньше. А вот когда бот идёт в прод — лучше ограничить его ровно тем набором прав, который ему реально нужен.
Если не получается зарегистрировать slash-команды (в консоли ошибка
50001: Missing Access) — скорее всего, вы забыли включить scopeapplications.commands, когда добавляли бота на сервер.Как и с любыми проблемами в Denizen, следите за консолью — отладочный вывод содержит много полезной информации, чтобы понять, что именно пошло не так.
Если застряли — спросите в Discord. Также загляните на общую страницу «Решение возникающих проблем»

