# Demote Plugin

#### What this plugin does

* Demotes a player to their **previous Roblox group rank**
* Logs demotions through **Panora API**
* Supports demoting:
  * a single user (by username)
  * an entire team via `%TeamName`

***

### 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 Explorer, locate:
   * `BasicAdminEssentials` → `Plugins`
2. Create a **ModuleScript** under `Plugins`
3. Name it something like: `demote`
4. Delete everything inside the ModuleScript and paste the updated code below:

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

```lua
--[[
───────────────────────────────────────────────────────────────
📦 Basic Admin Essentials Plugin — Demote 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 demote 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 demotions
	• WEBHOOK_URL → Optional; External webhook for demotion logs
	• MIN_USERNAME_LENGTH → Optional; minimum username length to demote
	• EveryoneAntiAbuse → Optional; prevents non-Everyone rank demotion

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 = "Demote 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 recentDemotions = {}

	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 getRolesSafe()
		local ok, roles = pcall(function()
			return GroupServiceModule.getGroupRoles(GROUP_ID)
		end)
		if not ok or type(roles) ~= "table" then
			return nil
		end
		return roles
	end

	local function findPreviousRank(currentRank)
		local roles = getRolesSafe()
		if not roles then return nil end

		local sortable = {}
		for _, r in pairs(roles) do
			if type(r) == "table" and type(r.Rank) == "number" then
				table.insert(sortable, r)
			end
		end
		table.sort(sortable, function(a, b)
			return a.Rank < b.Rank
		end)

		for i = #sortable, 1, -1 do
			if sortable[i].Rank < currentRank then
				return sortable[i]
			end
		end
		return nil
	end

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

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

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

		local last = recentDemotions[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 roles = getRolesSafe()
		if roles then
			for _, role in pairs(roles) do
				if role and role.Rank == currentRank then
					currentRoleName = role.Name or currentRoleName
					break
				end
			end
		end

		local prevRank = findPreviousRank(currentRank)
		if not prevRank then
			log("DEBUG", string.format("%s is already at lowest rank (or roles missing)", target.Name))
			return string.format("%s - Already at lowest 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 = prevRank.Rank,
			oldRankName = currentRoleName,
			command = "demote | 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", "Demotion API call errored for", target.Name)
			return string.format("%s - Failed (API error)", target.Name)
		end

		if success then
			recentDemotions[target.UserId] = os.time()
			log("PANORA", string.format("Demotion succeeded for %s (%s)", target.Name, tostring(message)))
			return string.format("%s - Success", target.Name)
		else
			log("PANORA", string.format("Demotion 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)
		if type(targets) ~= "table" then
			return "No valid targets."
		end

		local results = {}
		for _, t in pairs(targets) do
			if isPlayer(t) then
				table.insert(results, processDemotion(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 demotions 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("DEMOTE", string.format("Team demotion 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, set these 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**

***

### 5) Ensure Roblox settings are correct

To allow API requests:

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

Also make sure:

* Your `GROUP_ID` matches the group connected to your Panora workspace
* Your group’s roles/ranks are set correctly

***

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

#### Demote one player

Use your Basic Admin prefix (commonly `:`)

Examples:

* `:demote username` *(if your ModuleScript is named `demote`)*

**Important:** the command name is the **ModuleScript’s name**.

#### Demote an entire team

Use `%TeamName`

Example:

* `:demote %interns`

If no players are on that team:

* “No players found in the specified team.”

***

### Output / Results

You’ll get a per-user summary like:

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

***

### Troubleshooting

#### “API error” / Demotions failing

* Confirm **Allow HTTP Requests** is enabled
* Confirm API key is correct
* Confirm your game is **whitelisted** in Panora (Universal Game ID)
* Confirm the server/user has permission to edit group ranks

#### “Already at lowest rank”

That player is already at the lowest role in the group role list (or the plugin couldn’t load roles).

#### “Invalid target”

Basic Admin’s resolver returned something unexpected.\
This updated plugin **won’t crash**—it safely skips and shows this 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 demotions per user     | Optional |
| `WEBHOOK_URL`         | Webhook for demotion logs              | Optional |
| `MIN_USERNAME_LENGTH` | Minimum username length allowed        | Optional |
| `EveryoneAntiAbuse`   | Blocks `everyone` / `all` mass demotes | Optional |
