ZLDiscord v — Documentation


Introduction

ZLDiscord is a universal Paper 1.16.5–1.21.x plugin bridging Minecraft with Discord. Built-in template engine, LongPoll bot, custom commands, voting, DataStore and full PlaceholderAPI support.


Installation

  1. Copy ZoLiryzik.jar to plugins/
  2. Start LongPoll bot: node longpoll-server.js &
  3. Restart server (stop → start, no /reload)
  4. Configure config.yml: Discord Token, Guild ID, roles
  5. Connect LongPoll: /zlongpoll create demo http://localhost:17523/poll secret 25

v1.4 SQLite database with auto-migration — no setup required. Safe plugin reload with /zreload without server restart.


Commands

/zevent — Voting

/zevent "title" "command" "duration" "channel_id" [route] [resolve-now]
  • title — embed title (supports %placeholder%)
  • command — console command on ✅ win
  • duration5s, 10m, 1h
  • channel_id — Discord text channel ID

When the timer runs out, ✅ / ❌ are counted. If ✅ wins, the command executes.

  • route — server to execute on: player / main / server name | only discord server
  • resolve-nowtrue (default) / false. false = placeholders resolve when voting ends, not at creation

/zcc — Custom Commands

/zcc list             — list commands
/zcc run <name>       — execute
/zcc create <name>    — create (\n for newline)
/zcc remove <name>    — remove
/zcc reload           — reload
/zcc info <name>      — command info (template, cooldown)
/zcc prefix discord <p> — change Discord prefix
/zcc prefix mc <p>      — change MC prefix

/zlink / /zunlink — Discord Linking

/zlink — get code in Minecraft
/unlink — unlink in Discord (bot DMs)

/zembed — Embed Message

/zembed "name" "title" "description" "channel_id" [image] [thumbnail] [footer] [footer_icon]

Sends embed to Discord, saves messageId. PAPI

/zembedmanage — Embed Management

/zembedmanage delete <name>
/zembedmanage edit_title <name> "new title"
/zembedmanage edit_description <name> "new description"

/zattr — DataStore

/zattr get <bot|member|player> <key> [player]
/zattr set <bot|member|player> <key> <value> [player]
/zattr inc|dec <...> <key> <amount> [player]
/zattr list <bot|member|player> [player]

/zlongpoll — LongPoll

/zlongpoll create <name> <url> <key> [wait]
/zlongpoll list
/zlongpoll remove <name>
/zlongpoll reload

/zstatus — Bot Status

/zstatus                    — show status
/zstatus set <text>         — set status text
/zstatus settype <type>     — type: playing/watching/listening/competing
/zstatus reload             — reload bot_status.yml
/zstatus interval <sec>     — status rotation interval

v1.4 /zsync — GroupSync

/zsync reload    — reload group_sync.yml
/zsync status    — show sync status

Synchronizes Discord roles with LuckPerms groups. Assigns one Discord role based on player's LuckPerms group (with priority). Configured in group_sync.yml.

v1.4 /zprefix — Command Prefix

/zprefix discord <prefix>  — change Discord slash command prefix
/zprefix mc <prefix>      — change MC command prefix

Changes Minecraft command prefix (z by default) and Discord prefix (z! by default). Configured in config.ymlminecraft-command-prefix, discord-command-prefix, discord-slash-command-prefix.

v1.4 /zreload — Reload

/zreload

Safe plugin configuration reload without server restart. Re-reads config.yml, custom commands and LongPoll connections.

v1.4 Discord Role Management

/rolegive <member> <role>     — give role to member
/roleremove <member> <role>  — remove role from member
/rolemenu                       — open role selection menu
/roletoggle [member] [role]     — toggle role
  • /rolemenu — opens role selection panel in Discord (configured in config.yml)
  • /roletoggle — removes role if present, gives it if absent

Discord Commands

Prefix Text commands in Discord use z! prefix by default. Configurable in config.ymldiscord-command-prefix.

  • /zevent — create a vote (embed-creator roles)
  • z!embed — button for creating Embed
  • /link <code> — link account (bot DMs)
  • /unlink — unlink account (bot DMs)
  • /zcc reload — reload custom commands
  • /zcc list — list custom commands
  • /zcc prefix — manage prefix settings
  • Slash commands from custom_commands.yml register automatically
  • Text commands from custom_commands.yml use z! prefix (e.g. z!warn, z!report)

Permissions

PermissionDescription
zoliryzik.link v1.4 zoliryzik.zlinkLink account
zoliryzik.unlink v1.4 zoliryzik.zunlinkUnlink account
zoliryzik.event v1.4 zoliryzik.useCreate voting
zoliryzik.embed v1.4 zoliryzik.zembedCreate embed
zoliryzik.embed_manage v1.4 zoliryzik.zembed_manageManage embed
zoliryzik.ccCustom commands
zoliryzik.attr v1.4 zoliryzik.attr.getDataStore attributes
zoliryzik.statusBot status
zoliryzik.longpollLongPoll connections
zoliryzik.syncRole synchronization
zoliryzik.prefixChange prefix
zoliryzik.reloadReload plugin
zoliryzik.rolegiveGive Discord role
zoliryzik.roleremoveRemove Discord role
zoliryzik.rolemenuRole selection menu
zoliryzik.roletoggleToggle Discord role
zoliryzik.adminAdmin access (all commands)

Custom Commands

File custom_commands.yml:

commands:
  hello:
    file: "commands/hello.yml"
    description: "Greeting"
    cooldown: 10
    enabled: true
    scope: all              # all / minecraft / discord
    permission: zoliryzik.hello
    permission-message: "&cNo permission"
    command_mc: hello
    command_ds: hello        # Discord command name
    aliases_mc: ["h"]
    aliases_ds: ["h"]
    discord-roles: ["role_id_1", "role_id_2"]

Nested folders supported: commands/lp/lp.yml

hello.yml example

<~ args[0] ~>, hello from the server!

fly.yml example (conditions, arguments, PAPI)

<set target = args[0]>
<require target != null return "Specify player name">
<set mode = args[1]>
<if mode == null or mode == "">
  <set mode = "toggle">
</if>
<if mode == "on">
  <do mc.command("zflytoggle " + target + " on")>
  Fly &aenabled
<elseif mode == "off">
  <do mc.command("zflytoggle " + target + " off")>
  Fly &cdisabled
<else>
  <do mc.command("zflytoggle " + target)>
  Fly toggled
</if>

Template Engine

Variables

VariableScopeDescription
argsalwaysList of arguments
_args_rawalwaysRaw argument string
_cmd_namealwaysCommand name
_langalwaysLanguage: ru/en
mc_onlinealwaysOnline players
mc_max_playersalwaysMax players
mc_tpsalwaysTPS
mc_motdalwaysMOTD
_player_nameMinecraftPlayer name
_player_uuidMinecraftUUID
_player_worldMinecraftWorld
_discord_member_idDiscordMember ID
_discord_member_nameDiscordMember name
_discord_member_nickDiscordNickname
_discord_channel_idDiscordChannel ID
_discord_channel_nameDiscordChannel name
_discord_guild_idDiscordGuild ID
_discord_guild_nameDiscordGuild name
_mentioned_user_idDiscordMentioned user ID
_mentioned_user_nameDiscordMentioned user name

Tags

TagDescription
<~ expr ~>Print expression value
<set var = expr>Assign variable
<if cond>...<else>...</if>Condition
<for var in list>...</for>Loop
<do action(...)>Execute action
<require cond return "msg">Guard with error
<return "msg">Early return
<global var = expr>Global variable
<# comment #>Comment

v1.4 Conditions in <if> now support parentheses: <if (a == 1 or b == 2) and c == 3>

Functions

String: lower, upper, capitalize, trim, title, replace(a,b,s), split(sep,s), length, format, after(N)

Checks: defined, empty, not, number, iterable, even, odd, contains, startsWith, endsWith, typeof

Math: abs, round, number_format, random(min,max), random_hex, random_uuid

Collections: first, last, reverse, slice, sort, shuffle, batch, concat, join, keys, values, default

PAPI: papi("placeholder" [, player])

DataStore: getattribute(key), member.getattribute(key), player.getattribute(key), bot.getattribute(key), find_player_by_session(id)

HTTP: http_get(url), http_post(url, body|key,val...), http_put, http_delete, http_patch, http_code(resp), http_body(resp), http_header(name,val), http_bearer(token), json_parse(str)

Actions (<do>)

ActionDescription
mc.command("cmd")Execute command (console/player)
mc.broadcast("msg")Broadcast (& for colors)
mc.give("player","item")Give item
discord.send(channelId,"text")Send to Discord
discord.embed(...)Embed with fields, buttons, reactions
discord.role_add/give(memberId, roleId)Add role
discord.role_remove/delete(memberId, roleId)Remove role
discord.auto_delete()Auto-delete message with command
discord.open_modal("name")Open modal window
bot.setstatus("online/dnd/idle")Bot status
bot.setstatustext("text")Bot status text

Chain API

guild.getChannel("id").createEmbed().withTitle("...").withColor("#hex").withField("n","v",true).send()

Bot Status

File bot_status.yml:

enabled: true
type: "playing"     # playing / watching / listening / competing / custom
text: "with the server"

LongPoll Bot API

Node.js bot at http://localhost:17523. Auth: Authorization: Bearer secret.

MethodEndpointBody
POST/event{"type":"...", ...}
POST/api/config{"key":"value"}
GET/api/script?type=X
POST/api/script?type=X{"script":"..."}

LongPoll Scripts

Path: longpoll/papca/<connection>/<type>.yml

VariableDescription
_eventMap with all fields
_event.typeEvent type
_event.player, ...Fields from JSON
_conn_nameConnection name

Important Use <do action(...)>, NOT <action(...)>.

donate.yml example

<if !defined(_event.nickname)>
  <do mc.command("say", "Donate: no nickname in event")>
<else>
  <set _nick = _event.nickname>
  <set _amount = _event.amount>
  <do mc.command("give " + _nick + " diamond " + (_amount / 100))>
  <if _lang == "ru">
    <do mc.broadcast("&a🎉 Player " + _nick + " donated &e" + _amount + " &aand got " + (_amount / 100) + " diamonds!")>
  <else>
    <do mc.broadcast("&a🎉 Player " + _nick + " donated &e" + _amount + " &aand got " + (_amount / 100) + " diamonds!")>
  </if>
  <do player.getAttribute("donations").increment(_amount)>
</if>

PlaceholderAPI

  • Player — resolves for command sender
  • Console — resolves for first online player
  • papi("x", "Nick") — target any player by name
  • lang_*.yml%placeholder% works directly in command messages (getMessage(path, sender))
PlaceholderDescription
%zoliryzik_in_vc%Voice channel status: "In a voice channel" / "Not in a voice channel" / "Not linked"
%zoliryzik_attr_member_<key>%Discord member attribute from DataStore by key
%zoliryzik_attr_player_<key>%Player attribute from DataStore by key
%zoliryzik_attr_bot_<key>%Global bot attribute from DataStore by key

v1.4 Role Checks

PlaceholderDescription
%zoliryzik_role_<role_id>%"true" if player has ANY of the specified Discord role IDs (OR logic). Multiple IDs separated by commas.
%zoliryzik_roles_<role_id>%"true" if player has ALL of the specified Discord role IDs (AND logic). Multiple IDs separated by commas.

Example: %zoliryzik_role_123456,789012% — true if role 123456 OR 789012 is present.

Example: %zoliryzik_roles_123456,789012% — true if both role 123456 AND 789012 are present.


Configuration

Loading...

Environment Variables

VariableDescription
DISCORD_TOKENDiscord bot token
LONGPOLL_KEYAPI key (default: secret)
LONGPOLL_PORTPort (default: 17523)

GroupSync — Role Synchronization

File group_sync.yml. Synchronizes LuckPerms groups with Discord roles.

  • Priority: bottom to top. Last role = highest priority.
  • A player may have multiple LP groups, but in Discord they have only one role (highest priority).
  • sync-on-discord-edit: true — when a role is manually changed in Discord, the player's LP group is also updated.
enabled: true
sync-on-discord-edit: true
group-roles:
  default: "role_id_1"     # lowest priority
  vip: "role_id_2"
  premium: "role_id_3"
  admin: "role_id_4"       # highest priority

Reactions — Reaction Automation

File reactions.yml. Automatic actions when reactions are added/removed.

reactions:
  welcome:
    type: "reaction_add"
    channel_id: "000000000000000000"
    message_id: "any"
    emoji: "✅"
    role_id: "000000000000000000"
    remove_on_unreact: true

  upvote:
    type: "reaction_add"
    channel_id: "any"
    message_id: "any"
    emoji: "⭐"
    command: "mc.command('say '+_discord_member_name+' gave a star')"
  • typereaction_add or reaction_remove
  • channel_id — channel ID or "any"
  • message_id — message ID or "any"
  • role_id — role to assign/remove
  • command — template command to execute
  • remove_on_unreact — remove role when reaction is removed

Modals — Discord Modals

File modals.yml. Discord Modal — a popup window with input fields.

modals:
  report:
    title: "Submit Report"
    command: "report"
    fields:
      - label: "Player Name"
        style: "short"
        placeholder: "Enter nickname"
        required: true
        min_length: 1
        max_length: 32
      - label: "Reason"
        style: "paragraph"
        placeholder: "Describe the reason"
        required: true
        min_length: 10
        max_length: 500

Triggered from template: <do discord.open_modal("report")>

  • styleshort (single line) / paragraph (multi line)
  • Fields available in template as {field_label}

Banned Commands — Command Blacklist

File banned_commands.yml.

Two separate lists of banned commands:

zevent-blacklist

Moderators and event hosts cannot create polls for these commands.

zevent-blacklist:
  - stop
  - restart
  - shutdown
  - reload
  - plugman
  - op
  - deop
  - ban
  - kick
  - gamemode

mc-command-blacklist

Commands that cannot be executed via <do mc.command(...)>. Protection against template engine abuse.

mc-command-blacklist:
  # Server commands
  - stop
  - restart
  - shutdown
  - reload
  - plugman

  # Player permissions
  - op
  - deop
  - ban
  - pardon
  - ban-ip
  - pardon-ip

  # Server management
  - whitelist
  - save-all
  - save-off
  - save-on
  - force-upgrade
  - version
  - plugins
  - help

  # Gamemode and cheats
  - gamemode
  - gm
  - gmc
  - gms
  - gma
  - gmsp
  - effect
  - enchant
  - tp
  - teleport
  - tphere
  - tppos
  - spawnpoint
  - setworldspawn
  - time
  - weather
  - difficulty
  - xp

  # Dangerous operations
  - execute
  - run
  - as
  - data
  - datapack
  - function
  - macro

Custom Placeholders

File placeholders.yml. Create custom placeholders %zoliryzik_<name>%.

placeholders:
  server_type:
    description: "Server type"
    value: "Survival"

  online_info:
    description: "Online information"
    template: "&a❤ &f<~ mc_online ~>/<~ mc_max_players ~>"

  player_guild_name:
    description: "Player guild name"
    template: "<~ papi('%some_plugin_guild%', _player_name) ~>"
    requires_player: true
  • value — static value
  • template — dynamic template (supports <~ ~>)
  • requires_player — bind to player from context

Script Examples

Examples for sending events to the LongPoll bot from external scripts.

Python

import requests, json

URL = "http://localhost:17523/event"
HEADERS = {
    "Authorization": "Bearer secret",
    "Content-Type": "application/json"
}

# Send donate event
event = {
    "type": "donate",
    "nickname": "Player123",
    "amount": 1500,
    "product": "Diamond Pack"
}
r = requests.post(URL, headers=HEADERS, json=event)
print("Status:", r.status_code, r.json())

# Send kick event
event = {
    "type": "kick",
    "nickname": "Griefer",
    "reason": "Spam",
    "moderator": "Admin"
}
r = requests.post(URL, headers=HEADERS, json=event)
print("Status:", r.status_code, r.json())

Node.js

const URL = "http://localhost:17523/event";
const HEADERS = {
    "Authorization": "Bearer secret",
    "Content-Type": "application/json"
};

// Send donate event
fetch(URL, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
        type: "donate",
        nickname: "Player123",
        amount: 1500,
        product: "Diamond Pack"
    })
}).then(r => r.json()).then(console.log);

// Send kick event
fetch(URL, {
    method: "POST",
    headers: HEADERS,
    body: JSON.stringify({
        type: "kick",
        nickname: "Griefer",
        reason: "Spam",
        moderator: "Admin"
    })
}).then(r => r.json()).then(console.log);

curl

curl http://localhost:17523/event -X POST \
  -H "Authorization: Bearer secret" \
  -H "Content-Type: application/json" \
  -d '{"type":"donate","nickname":"Player123","amount":1500}'

curl http://localhost:17523/event -X POST \
  -H "Authorization: Bearer secret" \
  -H "Content-Type: application/json" \
  -d '{"type":"kick","nickname":"Griefer","reason":"Spam","moderator":"Admin"}'

Examples

LP command from Minecraft

<set _ = http_header("Authorization", "Bearer secret")>
<set _type = args[0]>
<set _resp = http_post("http://localhost:17523/event",
  "type", _type, "player", _player_name, "world", _player_world,
  "cmd", _cmd_name, "args", _args_raw)>
<do mc.broadcast("&a[LP] &f" + _type + " &a✓")>

Donate notification

<if _event.amount >= 5000>
  <do mc.broadcast("&a&l⭐ Mega-donate from &e" + _event.nickname)>
<else>
  <do mc.broadcast("&a❤ " + _event.nickname + " donated &e" + _event.amount)>
</if>

Player Info (info.yml)

<set target = args[0]>
<if target == null or target == "">
  <set target = _player_name>
</if>
📊 Info about <~ target ~>
├ Level: <~ papi("%level%", target) ~>
├ Health: <~ papi("%player_health%", target) ~>
├ World: <~ papi("%player_world%", target) ~>
└ Online: <~ mc_online ~>/<~ mc_max_players ~>

Report (Discord command)

<set target = args[0]>
<set reason = after(1)>
<require target != null and reason != "" return "Specify offender ID/mention and reason">
Report on <~ target ~>:
<~ reason ~>
<do discord.send(_discord_channel_id, format("%s, Your report has been submitted!", _discord_member_nick))>

Teleport request (Discord)

<set who = args[0]>
<require who != null return "Specify player name">
<require hasRole("MODERATOR_ROLE_ID") return "No permission for teleport">
<do mc.command("tp " + _player_name + " " + who)>
<do discord.send(_discord_channel_id,
  "⚡ " + _player_name + " teleported to " + who)>

Ban via Discord

<require hasRole("ADMIN_ROLE_ID") return "Admin only">
<set player = args[0]>
<set reason = after(1)>
<require player != null return "Specify player">
<require reason != "" return "Specify ban reason">
<do mc.command("ban " + player + " " + reason)>
<do discord.send(_discord_channel_id,
  "🔨 " + player + " banned. Reason: " + reason)>

Important hasRole() checks Discord roles, only works in Discord commands. For Minecraft use standard permissions.

Online list (from Discord)

Players online: <~ mc_online ~>/<~ mc_max_players ~>
TPS: <~ mc_tps ~>

Announcement to channels

<set msg = after(0)>
<require msg != "" return "Write a message after the command">
<do discord.send("CHANNEL_ID_1", msg)>
<do discord.send("CHANNEL_ID_2", msg)>
<do mc.broadcast("&a📢 Announcement sent to Discord")>

Link check before reward

<set linked = getattribute("linked_" + _player_uuid)>
<if linked == null or linked == "">
  <do mc.command("msg " + _player_name + " Link your account: /zlink")>
<else>
  <do mc.command("give " + _player_name + " diamond 1")>
  <do mc.broadcast("&a" + _player_name + " got a diamond for linking!")>
</if>

Bot status with placeholders

In bot_status.yml you can use placeholders:

enabled: true
type: "playing"
text: "on server | <~ mc_online ~> players"