# Promote Plugin

#### What this plugin does

* Promotes a player to their **next Roblox group rank**
* Logs promotions through **Panora API**
* Supports promoting:
  * a single user (by username)
  * an entire team via `%TeamName` (depending on your team names)

***

### 1) Open Roblox Studio

1. Open **Roblox Studio**
2. Open the place where **Basic Admin Essentials** is installed.

***

### 2) Insert the plugin ModuleScript

1. In the Explorer, find your Basic Admin Essentials folder:
   * `BasicAdminEssentials` → `Plugins`
2. Create a **ModuleScript** under `Plugins`
3. Name it something like: `promote`
4. Delete everything inside the ModuleScript and paste this code:

#### Basic Admin Essentials > Plugins > promote (ModuleScript)

```lua
--[[
───────────────────────────────────────────────────────────────
📦 Basic Admin Essentials Plugin — Promote Command
───────────────────────────────────────────────────────────────
Developed and maintained by: Panora Connect LLC
Website: https://panora.cc

This module integrates Panora Connect’s API with Basic Admin Essentials,
allowing workspace administrators to promote users directly in-game using
the official Panora ranking system.

───────────────────────────────────────────────────────────────
⚙️ Configuration Guide
───────────────────────────────────────────────────────────────
The configuration is stored at the top of this script.
You can set the following variables:
	
	• API_KEY  → Your Panora API key (from your workspace)
	• GROUP_ID → Your Roblox group ID
	• COOLDOWN_TIME → Optional; adds cooldown between promotions
	• WEBHOOK_URL → Optional; External webhook for promotion logs
	• MIN_USERNAME_LENGTH → Optional; minimum username length to promote
	• EveryoneAntiAbuse → Optional; prevents non-Everyone rank promotion

Once configured, simply place this plugin inside your Basic Admin
“Plugins” folder. No other dependencies need to be modified.

───────────────────────────────────────────────────────────────
💬 Support & Contact
───────────────────────────────────────────────────────────────
For assistance, please reach out through one of the following:

📧 Email: support@panora.cc  
🌐 Live Chat: https://app.panora.cc  
💬 Community Discord: https://discord.gg/panora  

───────────────────────────────────────────────────────────────
(c) 2025 Panora Connect LLC. All rights reserved.
Unauthorized redistribution or sale of this module is prohibited.
───────────────────────────────────────────────────────────────
]]

local HttpService = game:GetService("HttpService")
local GroupServiceModule = require(95677908346714)
local PanoraAPI = require(111358642560007)

local Plugin = function(...)
	local Data = {...}
	local remoteEvent = Data[1][1]
	local remoteFunction = Data[1][2]
	local returnPermissions = Data[1][3]
	local Commands = Data[1][4]
	local Prefix = Data[1][5]
	local actionPrefix = Data[1][6]
	local returnPlayers = Data[1][7]
	local cleanData = Data[1][8]
	local pluginName = script.Name
	local pluginPrefix = Prefix
	local pluginLevel = 2
	local pluginUsage = "<username>"
	local pluginDescription = "Promote a user | Panora"
	local BASE_URL = "https://api.panora.cc/v1/ranker"




	local API_KEY = "PANORA-*********************-API-********************"
	local GROUP_ID = 000000
	local COOLDOWN_TIME = 0
	local WEBHOOK_URL = ""
	local MIN_USERNAME_LENGTH = 2
	local EveryoneAntiAbuse = true
	
	
	
--─────────────────────────────────────────────────────────────────────────────────────────────────────────
-- ⚠️ DO NOT EDIT PAST THIS LINE UNLESS YOU KNOW WHAT YOU ARE DOING. WE DO NOT ASSIST WITH MODIFIED CODES!!
--─────────────────────────────────────────────────────────────────────────────────────────────────────────

	local recentPromotions = {}

	local function log(tag, ...)
		print(string.format("[%s]", tag), ...)
	end

	local function isPlayer(obj)
		return typeof(obj) == "Instance" and obj:IsA("Player")
	end

	local function safeName(obj)
		if isPlayer(obj) then return obj.Name end
		return tostring(obj)
	end

	local function findNextRank(currentRank)
		local ok, res = pcall(function()
			return GroupServiceModule.getNextRank(currentRank, GROUP_ID)
		end)
		if not ok then return nil end
		return res
	end

	local function processPromotion(player, target)
		if not isPlayer(target) then
			log("DEBUG", "Invalid target passed to processPromotion:", safeName(target))
			return string.format("%s - Skipped (Invalid target)", safeName(target))
		end

		log("PROMOTE", "Processing", target.Name)

		if isPlayer(player) and player.UserId == target.UserId then
			log("DEBUG", "Skipped self-promotion for", target.Name)
			return string.format("%s - Skipped (Cannot promote self)", target.Name)
		end

		local last = recentPromotions[target.UserId]
		if last and os.time() - last < COOLDOWN_TIME then
			local remaining = COOLDOWN_TIME - (os.time() - last)
			log("DEBUG", string.format("Cooldown active for %s (%ds left)", target.Name, remaining))
			return string.format("%s - Cooldown active (%ds left)", target.Name, remaining)
		end

		local okRank, currentRank = pcall(function()
			return GroupServiceModule.getUserGroupRank(target.UserId, GROUP_ID)
		end)
		if not okRank then
			return string.format("%s - Failed (Could not fetch current rank)", target.Name)
		end

		local currentRoleName = "Unknown"
		local okRoles, roles = pcall(function()
			return GroupServiceModule.getGroupRoles(GROUP_ID)
		end)
		if okRoles and type(roles) == "table" then
			for _, role in pairs(roles) do
				if role and role.Rank == currentRank then
					currentRoleName = role.Name or currentRoleName
					break
				end
			end
		end

		local nextRank = findNextRank(currentRank)
		if not nextRank then
			log("DEBUG", string.format("%s already at max rank (or next rank missing)", target.Name))
			return string.format("%s - Already at highest rank", target.Name)
		end

		local rankData = {
			rankerName = isPlayer(player) and player.Name or "Unknown",
			rankerId = isPlayer(player) and player.UserId or 0,
			rankeeName = target.Name,
			rankeeId = target.UserId,
			newRankId = nextRank.Rank,
			oldRankName = currentRoleName,
			command = "promote | BAE Integration",
			prefix = Prefix,
			webhookUrl = WEBHOOK_URL,
		}

		local okApi, success, message = pcall(function()
			return PanoraAPI.rankUser(BASE_URL, API_KEY, rankData)
		end)

		if not okApi then
			log("PANORA", "Promotion API call errored for", target.Name)
			return string.format("%s - Failed (API error)", target.Name)
		end

		if success then
			recentPromotions[target.UserId] = os.time()
			log("PANORA", string.format("Promotion succeeded for %s (%s)", target.Name, tostring(message)))
			return string.format("%s - Success", target.Name)
		else
			log("PANORA", string.format("Promotion failed for %s (%s)", target.Name, tostring(message)))
			return string.format("%s - Failed (%s)", target.Name, message or "Unknown error")
		end
	end

	local function processMultiple(player, targets)
		local results = {}

		if type(targets) ~= "table" then
			return "No valid targets."
		end

		for _, t in pairs(targets) do
			if isPlayer(t) then
				table.insert(results, processPromotion(player, t))
			else
				table.insert(results, string.format("%s - Skipped (Invalid target)", safeName(t)))
			end
		end

		local summary = table.concat(results, "\n")
		remoteEvent:FireClient(player, summary)
		return summary
	end

	local function pluginFunction(Args)
		local player = Args[1]
		local rawTarget = Args[3]

		if rawTarget == nil then
			return "Please provide a username or team prefix."
		end

		local targetText = tostring(rawTarget)
		local lowered = string.lower(targetText)

		if EveryoneAntiAbuse and (lowered == "everyone" or lowered == "all" or lowered == "others") then
			return "Anti-Abuse setting is enabled. Mass promotions are not allowed."
		end

		if #targetText < MIN_USERNAME_LENGTH then
			return string.format("Username must be at least %d characters long.", MIN_USERNAME_LENGTH)
		end

		if targetText:sub(1, 1) == "%" then
			local teamName = targetText:sub(2)
			local team = game.Teams:FindFirstChild(teamName)
			local victims = {}

			if not team then
				for _, t in pairs(game.Teams:GetChildren()) do
					if tostring(t.Name):sub(1, 1):lower() == tostring(teamName):sub(1, 1):lower() then
						team = t
						break
					end
				end
			end

			if team then
				for _, plr in pairs(game.Players:GetPlayers()) do
					if plr.Team == team then
						table.insert(victims, plr)
					end
				end
			end

			if #victims == 0 then
				return "No players found in the specified team."
			end

			log("PROMOTE", string.format("Team promotion started (%s) - %d users", team.Name, #victims))
			return processMultiple(player, victims)
		end

		local victims
		local okResolve, resolveResult = pcall(function()
			return returnPlayers(player, targetText)
		end)
		if okResolve then victims = resolveResult end

		if not victims or type(victims) ~= "table" or #victims == 0 then
			return "Couldn't find player."
		end

		return processMultiple(player, victims)
	end

	return pluginName, pluginFunction, pluginLevel, pluginPrefix, { pluginName, pluginUsage, pluginDescription }
end

return Plugin

```

***

### 3) Configure the plugin

At the top of the script, replace the config values:

#### Required

* `API_KEY` → your Panora API key
* `GROUP_ID` → your Roblox group ID

#### Optional (recommended)

* `COOLDOWN_TIME` → cooldown in seconds per target (ex: `20`)
* `WEBHOOK_URL` → send logs to a Discord/webhook endpoint
* `MIN_USERNAME_LENGTH` → blocks tiny inputs like “a”
* `EveryoneAntiAbuse` → blocks `others` / `all`

***

### 4) Get your API Key + whitelist your game

1. Go to **app.panora.cc**
2. Copy your **Panora API Key**
3. Paste it into `API_KEY`
4. In **Panora Dashboard → Panora API**, whitelist your game’s **Universal Game ID**\
   (If your game isn’t whitelisted, the API can fail even if the command runs.)

***

### 5) Ensure Roblox settings are correct

To allow API requests:

* **Game Settings → Security**
  * ✅ Enable **Allow HTTP Requests**

Also make sure:

* The Roblox **Group ID** matches the group linked to your Panora workspace
* The group has ranks set up correctly

***

### 6) How to use the command (in-game)

#### Promote one player

Use your Basic Admin prefix (default often `:`)

Examples:

* `:promote username` (if your script name is `promote`)

*(The command name is the ModuleScript’s name — if the ModuleScript is called `PanoraPromote`, that’s the command.)*

#### Promote an entire team

Use `%TeamName`

Example:

* `:promote %interns`

If no players are on that team:

* “No players found in the specified team.”

***

### Output / Results

The plugin returns a per-user summary like:

* `PlayerName - Success`
* `PlayerName - Failed (reason)`
* `PlayerName - Cooldown active (Xs left)`
* `PlayerName - Skipped (Invalid target)`

***

### Troubleshooting

#### “API error” / Promotions failing

* Verify HTTP requests are enabled
* Confirm API key is correct
* Confirm your game’s **Universal Game ID** is whitelisted in Panora
* Confirm the group exists and your server has permission to change ranks

#### “Invalid target”

This usually means Basic Admin’s resolver returned something unexpected.\
This updated plugin **won’t crash**—it will safely skip and show that message.

***

### Configuration reference

| Variable              | Description                             | Required |
| --------------------- | --------------------------------------- | -------- |
| `API_KEY`             | Panora API key from app.panora.cc       | ✅        |
| `GROUP_ID`            | Roblox group ID linked to workspace     | ✅        |
| `COOLDOWN_TIME`       | Seconds between promotions per user     | Optional |
| `WEBHOOK_URL`         | Webhook for promotion logs              | Optional |
| `MIN_USERNAME_LENGTH` | Minimum username length allowed         | Optional |
| `EveryoneAntiAbuse`   | Blocks `everyone` / `all` mass promotes | Optional |
