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
- Copy
ZoLiryzik.jartoplugins/ - Start LongPoll bot:
node longpoll-server.js & - Restart server (stop → start, no /reload)
- Configure
config.yml: Discord Token, Guild ID, roles - 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
- duration —
5s,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-now —
true(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.yml → minecraft-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 inconfig.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.yml → discord-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.ymlregister automatically - Text commands from
custom_commands.ymlusez!prefix (e.g.z!warn,z!report)
Permissions
| Permission | Description |
|---|---|
zoliryzik.link v1.4 zoliryzik.zlink | Link account |
zoliryzik.unlink v1.4 zoliryzik.zunlink | Unlink account |
zoliryzik.event v1.4 zoliryzik.use | Create voting |
zoliryzik.embed v1.4 zoliryzik.zembed | Create embed |
zoliryzik.embed_manage v1.4 zoliryzik.zembed_manage | Manage embed |
zoliryzik.cc | Custom commands |
zoliryzik.attr v1.4 zoliryzik.attr.get | DataStore attributes |
zoliryzik.status | Bot status |
zoliryzik.longpoll | LongPoll connections |
zoliryzik.sync | Role synchronization |
zoliryzik.prefix | Change prefix |
zoliryzik.reload | Reload plugin |
zoliryzik.rolegive | Give Discord role |
zoliryzik.roleremove | Remove Discord role |
zoliryzik.rolemenu | Role selection menu |
zoliryzik.roletoggle | Toggle Discord role |
zoliryzik.admin | Admin 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
| Variable | Scope | Description |
|---|---|---|
args | always | List of arguments |
_args_raw | always | Raw argument string |
_cmd_name | always | Command name |
_lang | always | Language: ru/en |
mc_online | always | Online players |
mc_max_players | always | Max players |
mc_tps | always | TPS |
mc_motd | always | MOTD |
_player_name | Minecraft | Player name |
_player_uuid | Minecraft | UUID |
_player_world | Minecraft | World |
_discord_member_id | Discord | Member ID |
_discord_member_name | Discord | Member name |
_discord_member_nick | Discord | Nickname |
_discord_channel_id | Discord | Channel ID |
_discord_channel_name | Discord | Channel name |
_discord_guild_id | Discord | Guild ID |
_discord_guild_name | Discord | Guild name |
_mentioned_user_id | Discord | Mentioned user ID |
_mentioned_user_name | Discord | Mentioned user name |
Tags
| Tag | Description |
|---|---|
<~ 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>)
| Action | Description |
|---|---|
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.
| Method | Endpoint | Body |
|---|---|---|
| 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
| Variable | Description |
|---|---|
_event | Map with all fields |
_event.type | Event type |
_event.player, ... | Fields from JSON |
_conn_name | Connection 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))
| Placeholder | Description |
|---|---|
%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
| Placeholder | Description |
|---|---|
%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
| Variable | Description |
|---|---|
DISCORD_TOKEN | Discord bot token |
LONGPOLL_KEY | API key (default: secret) |
LONGPOLL_PORT | Port (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')"
type—reaction_addorreaction_removechannel_id— channel ID or"any"message_id— message ID or"any"role_id— role to assign/removecommand— template command to executeremove_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")>
style—short(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 valuetemplate— 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"