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


---

# 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/demote.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.
