Skip to Content
TutorialsDiscord Slash Command Tutorial

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 end

This 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 linked
  • e:getCommandName() - The slash command name that was invoked
  • e:getParameter(name) - Get parameter values
  • e:thinking() - Show “Bot is thinking…” message
  • e:thinking(1) - Show ephemeral (only visible to user) thinking message
  • e:editOriginalResponse() - Edit the response after thinking
  • e: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 end

Validation Steps:

  1. Get parameter value
  2. Check if empty
  3. Search for players
  4. Handle “not found” case
  5. 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 end

Logic:

  1. If multiple players found, try exact match first
  2. If no exact match, show up to 3 suggestions
  3. 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() } end

Button 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

  1. Save this script to your server’s Lua scripts folder
  2. Restart your server or reload scripts
  3. Wait for the bot to come online (check console for “Discord bot is ready”)
  4. In Discord, type /punish and you’ll see the command
  5. Enter a player name and press Enter
  6. 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!

Last updated on