# 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 |


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.panora.cc/api/basic-admin/promote.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
