Discord Slash Command Tutorial
This tutorial demonstrates how to create a Discord bot with slash commands that can interact with your GTPS server. We’ll build a player punishment system with interactive buttons.
Overview
This example shows:
- Registering Discord slash commands when the bot is ready
- Handling slash command invocations
- User input validation and player lookup
- Interactive buttons for ban/unban actions
- Ephemeral “thinking” states for better UX
Full Example Script
-- discord-example script
print("(Loaded) discord-example script for GTPS Cloud")
function findMatch(avList, searchName)
for _, player in ipairs(avList) do
if player:getCleanName():lower() == searchName:lower() then
return {player}
end
end
return nil
end
onDiscordBotReadyCallback(function()
print("Discord bot is ready")
DiscordBot.globalCommandCreate("punish", "Punish player", {
{ type="string", name="player_name", description="Player name", required=true }
})
end)
onDiscordSlashCommandCallback(function(e)
local player = e:getPlayer() -- If user has acc linked it will return valid GTPS player
local commandName = e:getCommandName()
local channelID = e:getChannelID()
local authorID = e:getAuthorID()
print("Received slash command event, cName: " .. commandName .. " cId: " .. channelID .. " aId: " .. authorID)
if commandName == "punish" then
e:thinking() -- Use e:thinking(1) to make it ephemeral, thinking is not required at all
-- If u dont want to use e:thinking then use e:reply() instead of e:editOriginalResponse() below.
local playerName = e:getParameter("player_name")
if playerName == "" then
e:editOriginalResponse(channelID, "Oops: You need to enter player name.")
return
end
local foundPlayers = getPlayerByName(playerName)
if #foundPlayers == 0 then
e:editOriginalResponse(channelID, "Oops: There is nobody currently in this server with a name starting with " .. playerName .. ".")
return
end
if #foundPlayers > 1 then
local matchedPlayers = findMatch(foundPlayers, playerName)
if matchedPlayers then
foundPlayers = matchedPlayers
else
local possible_matches = {}
local limit = math.min(#foundPlayers, 3)
for i = 1, limit do
table.insert(possible_matches, foundPlayers[i]:getName())
end
local extra_count = #foundPlayers - 3
local extra_info = extra_count > 0 and (" and " .. formatNum(extra_count) .. " more...") or "."
e:editOriginalResponse(channelID, "Error, more than one person's name in this server starts with " .. playerName .. ". Be more specific. Possible matches: " .. table.concat(possible_matches, ", ") .. extra_info)
return
end
end
local targetPlayer = foundPlayers[1]
local isBanned = targetPlayer:hasMod(-75)
local button
if isBanned then
button = {
type = "button",
label = "Unban",
style = "success",
emoji = "😇",
id = "unban_usr_" .. targetPlayer:getUserID()
}
else
button = {
type = "button",
label = "Ban 60 days",
style = "danger",
emoji = "😭",
id = "ban_usr_" .. targetPlayer:getUserID()
}
end
e:editOriginalResponse(
channelID,
"**" .. targetPlayer:getCleanName() .. " (ID " .. targetPlayer:getUserID() .. ") - Banned = " .. tostring(isBanned) .. "**",
{ button }
)
end
end)
onDiscordButtonClickCallback(function(e)
local customID = e:getCustomID()
if string.sub(customID, 1, 8) == "ban_usr_" then
local userID = tonumber(string.sub(customID, 9))
local player = getPlayer(userID)
player:addMod(-75, 60 * 24 * 60 * 60)
e:reply("Player **" .. player:getCleanName() .. "** has been banned for 60 days. 😭")
elseif string.sub(customID, 1, 10) == "unban_usr_" then
local userID = tonumber(string.sub(customID, 11))
local player = getPlayer(userID)
player:removeMod(-75)
e:reply("Player **" .. player:getCleanName() .. "** has been unbanned. 😇")
end
end)Breaking Down the Code
1. Helper Function - Exact Name Match
function findMatch(avList, searchName)
for _, player in ipairs(avList) do
if player:getCleanName():lower() == searchName:lower() then
return {player}
end
end
return nil
endThis function finds an exact match for a player name (case-insensitive). It’s used when multiple players have similar names.
2. Bot Ready Callback
onDiscordBotReadyCallback(function()
print("Discord bot is ready")
DiscordBot.globalCommandCreate("punish", "Punish player", {
{ type="string", name="player_name", description="Player name", required=true }
})
end)Key Points:
- This callback fires when your Discord bot comes online
- Perfect place to register slash commands
- Commands are registered globally and will appear in Discord
- Parameter types:
string,integer,boolean,user,channel,role
3. Slash Command Handler
onDiscordSlashCommandCallback(function(e)
local player = e:getPlayer() -- Linked GTPS player (if available)
local commandName = e:getCommandName()
local channelID = e:getChannelID()
local authorID = e:getAuthorID()Available Methods:
e:getPlayer()- Returns GTPS player if Discord account is linkede:getCommandName()- The slash command name that was invokede:getParameter(name)- Get parameter valuese:thinking()- Show “Bot is thinking…” messagee:thinking(1)- Show ephemeral (only visible to user) thinking messagee:editOriginalResponse()- Edit the response after thinkinge:reply()- Send immediate reply (use instead of thinking + edit)
4. Player Lookup & Validation
local playerName = e:getParameter("player_name")
if playerName == "" then
e:editOriginalResponse(channelID, "Oops: You need to enter player name.")
return
end
local foundPlayers = getPlayerByName(playerName)
if #foundPlayers == 0 then
e:editOriginalResponse(channelID, "Oops: There is nobody currently in this server with a name starting with " .. playerName .. ".")
return
endValidation Steps:
- Get parameter value
- Check if empty
- Search for players
- Handle “not found” case
- Handle multiple matches case
5. Multiple Match Handling
if #foundPlayers > 1 then
local matchedPlayers = findMatch(foundPlayers, playerName)
if matchedPlayers then
foundPlayers = matchedPlayers
else
-- Show possible matches
local possible_matches = {}
local limit = math.min(#foundPlayers, 3)
for i = 1, limit do
table.insert(possible_matches, foundPlayers[i]:getName())
end
local extra_count = #foundPlayers - 3
local extra_info = extra_count > 0 and (" and " .. formatNum(extra_count) .. " more...") or "."
e:editOriginalResponse(channelID, "Error, more than one person's name in this server starts with " .. playerName .. ". Be more specific. Possible matches: " .. table.concat(possible_matches, ", ") .. extra_info)
return
end
endLogic:
- If multiple players found, try exact match first
- If no exact match, show up to 3 suggestions
- Tell user how many more matches exist
6. Dynamic Button Generation
local targetPlayer = foundPlayers[1]
local isBanned = targetPlayer:hasMod(-75)
local button
if isBanned then
button = {
type = "button",
label = "Unban",
style = "success",
emoji = "😇",
id = "unban_usr_" .. targetPlayer:getUserID()
}
else
button = {
type = "button",
label = "Ban 60 days",
style = "danger",
emoji = "😭",
id = "ban_usr_" .. targetPlayer:getUserID()
}
endButton Styles:
primary- Blue (default action)success- Green (positive action)danger- Red (destructive action)secondary- Gray (neutral action)
Important: The id field encodes the user ID so the button callback knows which player to target.
7. Button Click Handler
onDiscordButtonClickCallback(function(e)
local customID = e:getCustomID()
if string.sub(customID, 1, 8) == "ban_usr_" then
local userID = tonumber(string.sub(customID, 9))
local player = getPlayer(userID)
player:addMod(-75, 60 * 24 * 60 * 60)
e:reply("Player **" .. player:getCleanName() .. "** has been banned for 60 days. 😭")
elseif string.sub(customID, 1, 10) == "unban_usr_" then
local userID = tonumber(string.sub(customID, 11))
local player = getPlayer(userID)
player:removeMod(-75)
e:reply("Player **" .. player:getCleanName() .. "** has been unbanned. 😇")
end
end)Key Techniques:
- Extract user ID from button’s custom ID
- Use
string.sub()to parse the ID format - Apply mod (punishment) with duration in seconds
- Send confirmation reply
Usage
- Save this script to your server’s Lua scripts folder
- Restart your server or reload scripts
- Wait for the bot to come online (check console for “Discord bot is ready”)
- In Discord, type
/punishand you’ll see the command - Enter a player name and press Enter
- Click the Ban/Unban button to apply the action
Extending This Example
Add More Commands:
onDiscordBotReadyCallback(function()
DiscordBot.globalCommandCreate("punish", "Punish player", {
{ type="string", name="player_name", description="Player name", required=true }
})
DiscordBot.globalCommandCreate("kick", "Kick player", {
{ type="string", name="player_name", description="Player name", required=true }
})
DiscordBot.globalCommandCreate("give", "Give items to player", {
{ type="string", name="player_name", description="Player name", required=true },
{ type="integer", name="item_id", description="Item ID", required=true },
{ type="integer", name="amount", description="Amount", required=true }
})
end)Add Permission Checks:
onDiscordSlashCommandCallback(function(e)
local authorID = e:getAuthorID()
-- Check if user has admin role (replace with your role ID)
if not hasDiscordRole(authorID, "1234567890123456789") then
e:reply("You don't have permission to use this command!", nil, ReplyFlags.EPHEMERAL)
return
end
-- ... rest of command logic
end)Notes
- Component structure now uses rows (old flat format doesn’t work)
- Use
e:thinking()for commands that take time to process - Use
e:reply()for instant responses - Custom IDs should encode necessary data for the button callback
- Ban duration is in seconds:
60 * 24 * 60 * 60= 60 days
✅
Discord Integration Complete
You now have a working Discord bot that can manage players through slash commands and interactive buttons!