Skip to content
Open
Show file tree
Hide file tree
Changes from 5 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
035d8af
Adds a moderation action history into the player submenus without any…
DukeOfCheese Apr 13, 2025
aa3e543
Refactor action deletion logic to use action ID and improve action hi…
DukeOfCheese Apr 13, 2025
5013313
Enhance action logging by adding detailed ban and unban information t…
DukeOfCheese Apr 13, 2025
3305aaf
Normalize action names in logging: use consistent casing for actions …
DukeOfCheese Apr 13, 2025
0c02a69
Add unban option to action history for banned players
DukeOfCheese Apr 14, 2025
4fb2aaf
Implement ban storage functionality and integrate ban addition in off…
DukeOfCheese Apr 15, 2025
6b5e511
Merge branch 'master' of https://github.com/DukeOfCheese/EasyAdmin
DukeOfCheese Apr 15, 2025
d1481ec
Update loading item text in action history menu for better localization
DukeOfCheese Apr 15, 2025
3680508
Refactor action logging to ensure action history is recorded consiste…
DukeOfCheese Apr 15, 2025
76355de
Update English localization for improved clarity and consistency
DukeOfCheese Apr 15, 2025
ed5b1eb
Merge branch 'Blumlaut:master' into master
DukeOfCheese Apr 20, 2025
989b2c7
feat(storage): implement banlist and actions management functions
DukeOfCheese Apr 20, 2025
73e1691
feat(storage): refactor ban and action management functions for impro…
DukeOfCheese Apr 20, 2025
0156946
feat(storage): refactor action history management to utilize storage …
DukeOfCheese Apr 21, 2025
0abecb4
Merge branch 'Blumlaut:master' into master
DukeOfCheese Apr 22, 2025
8b0d481
feat(storage): add action logging functionality and permissions for a…
DukeOfCheese Apr 22, 2025
4560358
Merge branch 'master' of https://github.com/DukeOfCheese/EasyAdmin
DukeOfCheese Apr 22, 2025
cd5ff0a
fix(warnPlayer): correct permission check logic for warning players
DukeOfCheese Apr 22, 2025
74645cd
feat(action-logging): implement action logging for moderation events
DukeOfCheese Apr 22, 2025
df983c8
feat(storage): enhance storage functionality with new ban management …
DukeOfCheese Apr 23, 2025
7c1af0b
fix(banlist, storage): remove unnecessary backticks and clarify updat…
DukeOfCheese May 12, 2025
b365a51
Merge branch 'master' into master
DukeOfCheese Jun 6, 2025
d670b3d
refactor(storage): remove unused notes variable and fix getBanlist fu…
DukeOfCheese Jun 6, 2025
ac60157
fix(gui): correct localization string for action history submenu
DukeOfCheese Jun 6, 2025
9e6ba5f
fix(gui): correct string concatenation in action history submenu
DukeOfCheese Jun 8, 2025
f33334d
fix(gui): correct string concatenation in action history submenu
DukeOfCheese Jun 8, 2025
5f1a07c
Merge branch 'master' of https://github.com/DukeOfCheese/EasyAdmin
DukeOfCheese Jun 8, 2025
2e1a357
fix(kick): streamline action logging by using Storage for kick actions
DukeOfCheese Jun 8, 2025
f4e7c15
fix(ban): improve ban action logging and standardize identifier usage
DukeOfCheese Jun 9, 2025
ca31e92
fix(ban): update action logging to use discord identifiers for ban ac…
DukeOfCheese Jun 9, 2025
bb055dc
Merge branch 'master' into master
DukeOfCheese Jun 11, 2025
ba05825
Merge branch 'master' into master
DukeOfCheese Jul 2, 2025
21cf025
(update): Added preliminary version control
DukeOfCheese Aug 31, 2025
148bf6b
(update): Remove comments
DukeOfCheese Aug 31, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
64 changes: 63 additions & 1 deletion client/gui_c.lua
Original file line number Diff line number Diff line change
Expand Up @@ -827,6 +827,68 @@ function GenerateMenu() -- this is a big ass function
TriggerEvent("EasyAdmin:showNotification", GetLocalisedText("nodiscordpresent"))
end
end

if GetConvar("ea_enableActionHistory", "true") == "true" and permissions["player.actionhistory.view"] then
local actionHistoryMenu = _menuPool:AddSubMenu(thisPlayer, GetLocalisedText("actionhistory"), GetLocalisedText("actionhistoryguide"), true)
actionHistoryMenu:SetMenuWidthOffset(menuWidth)
local loadingItem = NativeUI.CreateItem("Loading...", "Please wait while we fetch the data.")
actionHistoryMenu:AddItem(loadingItem)
TriggerServerEvent("EasyAdmin:GetActionHistory", thePlayer.discord)

RegisterNetEvent("EasyAdmin:ReceiveActionHistory")
AddEventHandler("EasyAdmin:ReceiveActionHistory", function(actionHistory)
actionHistoryMenu:Clear()
if #actionHistory == 0 then
local noActionsItem = NativeUI.CreateItem(GetLocalisedText("noactions"), GetLocalisedText("noactionsguide"))
actionHistoryMenu:AddItem(noActionsItem)
end
for i, action in ipairs(actionHistory) do
local actionSubmenu = _menuPool:AddSubMenu(actionHistoryMenu, "["..action.id.."] " .. action.action .. " by " .. action.moderator, "Reason: " .. action.reason or "", true)
actionSubmenu:SetMenuWidthOffset(menuWidth)
if action.action == "BAN" and permissions["player.ban.remove"] then
local actionUnban = NativeUI.CreateItem(GetLocalisedText("unbanplayer"), GetLocalisedText("unbanplayerguide"))
actionUnban.Activated = function(ParentMenu, SelectedItem)
TriggerServerEvent("EasyAdmin:UnbanPlayer", action.id)
TriggerEvent("EasyAdmin:showNotification", GetLocalisedText("unbanplayer"))
TriggerServerEvent("EasyAdmin:GetActionHistory", thePlayer.discord)
ParentMenu:Visible(false)
ParentMenu.ParentMenu:Visible(true)
end
actionSubmenu:AddItem(actionUnban)
end
if permissions["player.actionhistory.delete"] then
local actionDelete = NativeUI.CreateItem(GetLocalisedText("deleteaction"), GetLocalisedText("deleteactionguide"))
actionDelete.Activated = function(ParentMenu, SelectedItem)
TriggerServerEvent("EasyAdmin:DeleteAction", action.id)
TriggerEvent("EasyAdmin:showNotification", GetLocalisedText("actiondeleted"))
TriggerServerEvent("EasyAdmin:GetActionHistory", thePlayer.discord)
ParentMenu:Visible(false)
ParentMenu.ParentMenu:Visible(true)
end
actionSubmenu:AddItem(actionDelete)
end
local punishedDiscord = NativeUI.CreateItem(GetLocalisedText("getplayerdiscord"), GetLocalisedText("getplayerdiscordguide"))
punishedDiscord.Activated = function(ParentMenu, SelectedItem)
if action.discord then
copyToClipboard(action.discord)
else
TriggerEvent("EasyAdmin:showNotification", GetLocalisedText("nodiscordpresent"))
end
end
actionSubmenu:AddItem(punishedDiscord)
local moderatorDiscord = NativeUI.CreateItem(GetLocalisedText("getmoderatordiscord"), GetLocalisedText("getmoderatordiscordguide"))
moderatorDiscord.Activated = function(ParentMenu, SelectedItem)
if action.moderatorId then
copyToClipboard(action.moderatorId)
else
TriggerEvent("EasyAdmin:showNotification", GetLocalisedText("nodiscordpresent"))
end
end
actionSubmenu:AddItem(moderatorDiscord)
actionSubmenu:RefreshIndex()
end
end)
end

ExecutePluginsFunction("playerMenu", thePlayer.id)

Expand Down Expand Up @@ -2002,4 +2064,4 @@ function DrawPlayerInfoLoop()
end
end)

end
end
2 changes: 2 additions & 0 deletions fxmanifest.lua
Original file line number Diff line number Diff line change
Expand Up @@ -88,5 +88,7 @@ convar_category 'EasyAdmin' {
{ "Channel for Discord bot to enable live status", "$ea_botStatusChannel", "CV_STRING", "true" },
{ "Enable Allowlist", "$ea_enableAllowlist", "CV_BOOL", "false" },
{ "Routing Bucket Options", "$ea_routingBucketOptions", "CV_BOOL", "false" },
{ "Enable Action History", "$ea_enableActionHistory", "CV_BOOL", "true" },
{ "Action History Expiry", "$ea_actionHistoryExpiry", "CV_INT", "30"}, -- Recommended time is 30 days
}
}
12 changes: 12 additions & 0 deletions language/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,18 @@
"bucketguide": "Forces either player or yourself into the other's routing bucket.",
"playerbucketjoined": "Joined player bucket.",
"playerbucketforced": "Player has been forced to your bucket.",

"actionhistory": "Action History",
"actionhistoryguide": "View moderation actions against this user.",
"noactions": "No actions found.",
"noactionsguide": "No past moderation actions for this user.",
"deleteaction": "Delete Action",
"deleteactionguide": "Delete this action from the history.",
"actiondeleted": "Action deleted.",
"getplayerdiscord": "Copy Discord of Player",
"getplayerdiscordguide": "Copy the Discord ID of the Player.",
"getmoderatordiscord": "Copy Discord of Moderator",
"getmoderatordiscordguide": "Copy the Discord ID of the Moderator.",

"copydiscord": "Copy Discord ID",
"discordcopied": "Discord ID copied to clipboard.",
Expand Down
188 changes: 188 additions & 0 deletions server/action_history.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,188 @@
------------------------------------
------------------------------------
---- DONT TOUCH ANY OF THIS IF YOU DON'T KNOW WHAT YOU ARE DOING
---- THESE ARE **NOT** CONFIG VALUES, USE THE CONVARS IF YOU WANT TO CHANGE SOMETHING
----
----
---- If you are a developer and want to change something, consider writing a plugin instead:
---- https://easyadmin.readthedocs.io/en/latest/plugins/
----
------------------------------------
------------------------------------

local actions = {}

local function SaveActions(actions)

end

RegisterNetEvent("EasyAdmin:GetActionHistory", function(discordId)
if DoesPlayerHavePermission(source, "player.actionhistory.view") then
if not discordId then
PrintDebugMessage("No Discord ID provided, returning empty action history.", 2)
TriggerClientEvent("EasyAdmin:ReceiveActionHistory", source, {})
return
end
local history = {}
if actions then
for i, action in ipairs(actions) do
if tostring(action.discord) == tostring(discordId) then
table.insert(history, {
action = action.action,
reason = action.reason,
discord = action.discord,
moderator = action.moderator,
moderatorId = action.moderatorId,
time = action.time,
})
end
end
else
PrintDebugMessage("No actions found in the history.", 2)
end
TriggerClientEvent("EasyAdmin:ReceiveActionHistory", source, history)
else
PrintDebugMessage("Player does not have permission to view action history.", 2)
TriggerClientEvent("EasyAdmin:ReceiveActionHistory", source, {})
end
end)

RegisterNetEvent("EasyAdmin:DeleteAction", function(actionId)
if DoesPlayerHavePermission(source, "player.actionhistory.delete") then
if not actionId then
PrintDebugMessage("Invalid parameters provided for action deletion.", 2)
return
end
for i, act in ipairs(actions) do
if act.id == actionId then
table.remove(actions, i)
local saved = SaveResourceFile(GetCurrentResourceName(), "actions.json", json.encode(actions, {indent = true}), -1)
if not saved then
PrintDebugMessage("^1Saving actions.json failed! Please check if EasyAdmin has Permission to write in its own folder!^7", 1)
end
PrintDebugMessage("Removed action: " .. json.encode(act), 4)
end
local saved = SaveResourceFile(GetCurrentResourceName(), "actions.json", json.encode(actions, {indent = true}), -1)
if not saved then
PrintDebugMessage("^1Saving actions.json failed! Please check if EasyAdmin has Permission to write in its own folder!^7", 1)
end
end
PrintDebugMessage("No matching action found for deletion.", 2)
else
PrintDebugMessage("Player does not have permission to delete actions.", 2)
end
end)

AddEventHandler("EasyAdmin:LogAction", function(data, remove, forceChange)
local change = (forceChange or false)
local content = LoadResourceFile(GetCurrentResourceName(), "actions.json")
if not content then
PrintDebugMessage("actions.json file was missing, we created a new one.", 2)
local saved = SaveResourceFile(GetCurrentResourceName(), "actions.json", json.encode({}), -1)
if not saved then
PrintDebugMessage("^1Saving actions.json failed! Please check if EasyAdmin has Permission to write in its own folder!^7", 1)
end
content = json.encode({})
end
actions = json.decode(content)

if not actions then
PrintDebugMessage("^1-^2-^3-^4-^5-^6-^8-^9-^1-^2-^3-^4-^5-^6-^8-^9-^1-^2-^3-^3!^1FATAL ERROR^3!^3-^2-^1-^9-^8-^6-^5-^4-^3-^2-^1-^9-^8-^6-^5-^4-^3-^2-^7\n")
PrintDebugMessage("^1Failed^7 to load Actions!\n")
PrintDebugMessage("Please check your actions file for errors, ^Action history *will not* work!^7\n")
PrintDebugMessage("^1-^2-^3-^4-^5-^6-^8-^9-^1-^2-^3-^4-^5-^6-^8-^9-^1-^2-^3-^3!^1FATAL ERROR^3!^3-^2-^1-^9-^8-^6-^5-^4-^3-^2-^1-^9-^8-^6-^5-^4-^3-^2-^7\n")
return
end

if data and not remove then
if data.action == "ban" then
table.insert(actions, {
time = os.time(),
id = #actions + 1,
banId = data.banId,
action = data.action,
discord = data.discord,
reason = data.reason,
moderator = data.moderator,
moderatorId = data.moderatorId,
expire = data.expire,
expireString = data.expireString
})
elseif data.action == "offline ban" then
table.insert(actions, {
time = os.time(),
id = #actions + 1,
banId = data.banId,
action = data.action,
discord = data.discord,
reason = data.reason,
moderator = data.moderator,
moderatorId = data.moderatorId,
expire = data.expire,
expireString = data.expireString
})
elseif data.action == "kick" then
table.insert(actions, {
time = os.time(),
id = #actions + 1,
action = data.action,
discord = data.discord,
reason = data.reason,
moderator = data.moderator,
moderatorId = data.moderatorId,
})
elseif data.action == "warn" then
table.insert(actions, {
time = os.time(),
id = #actions + 1,
action = data.action,
discord = data.discord,
reason = data.reason,
moderator = data.moderator,
moderatorId = data.moderatorId,
})
elseif data.action == "unban" then
for i, act in ipairs(actions) do
if act.banId == data.banId then
act["action"] = data.action
break
end
end
end
PrintDebugMessage("Added the following to actions:\n"..table_to_string(data), 4)
change=true
elseif not data then
return
end
if data and remove then
PrintDebugMessage("Removed the following data from actions:\n"..table_to_string(data), 4)
change = true
end
if change then
PrintDebugMessage("Actions changed, saving..", 4)
local saved = SaveResourceFile(GetCurrentResourceName(), "actions.json", json.encode(actions, {indent = true}), -1)
if not saved then
PrintDebugMessage("^1Saving actions.json failed! Please check if EasyAdmin has Permission to write in its own folder!^7", 1)
end
end
PrintDebugMessage("Completed Actions Updated.", 4)
end)

for i, action in ipairs(actions) do
if action.time + (GetConvar("ea_actionHistoryExpiry", 30) * 24 * 60 * 60) < os.time() then
table.remove(actions, i)
PrintDebugMessage("Removed expired action: " .. json.encode(action), 4)
end
end

local change = (forceChange or false)
local content = LoadResourceFile(GetCurrentResourceName(), "actions.json")
if not content then
PrintDebugMessage("actions.json file was missing, we created a new one.", 2)
local saved = SaveResourceFile(GetCurrentResourceName(), "actions.json", json.encode({}), -1)
if not saved then
PrintDebugMessage("^1Saving actions.json failed! Please check if EasyAdmin has Permission to write in its own folder!^7", 1)
end
content = json.encode({})
end
actions = json.decode(content)
21 changes: 18 additions & 3 deletions server/admin_server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,14 @@ Citizen.CreateThread(function()
reason = formatShortcuts(reason)
SendWebhookMessage(moderationNotification,string.format(GetLocalisedText("adminkickedplayer"), getName(source, false, true), getName(playerId, true, true), reason), "kick", 16711680)
PrintDebugMessage("Kicking Player "..getName(source, true).." for "..reason, 3)
if GetConvar("ea_enableActionHistory", "true") == "true" then
local playerDiscord = GetPlayerIdentifierByType(playerId, 'discord')
local discordId
if playerDiscord then
discordId = playerDiscord:match("discord:(%d+)")
TriggerEvent("EasyAdmin:LogAction", { action = "kick", license = discordId, reason = reason, banner = getName(source, true, true)})
end
end
DropPlayer(playerId, string.format(GetLocalisedText("kicked"), getName(source), reason) )
elseif CachedPlayers[playerId].immune then
TriggerClientEvent("EasyAdmin:showNotification", source, GetLocalisedText("adminimmune"))
Expand Down Expand Up @@ -757,7 +765,7 @@ Citizen.CreateThread(function()

RegisterServerEvent("EasyAdmin:warnPlayer", function(id, reason)
local src = source
if DoesPlayerHavePermission(src,"player.warn") and not CachedPlayers[id].immune and CheckAdminCooldown(source, "warn") then
if DoesPlayerHavePermission(src,"player.warn") and CachedPlayers[id].immune and CheckAdminCooldown(source, "warn") then
SetAdminCooldown(source, "warn")
reason = formatShortcuts(reason)
local maxWarnings = GetConvarInt("ea_maxWarnings", 3)
Expand All @@ -771,11 +779,17 @@ Citizen.CreateThread(function()
})
TriggerClientEvent("txcl:showWarning", id, getName(src), string.format(GetLocalisedText("warned"), reason, WarnedPlayers[id].warns, maxWarnings), GetLocalisedText("warnedtitle"), GetLocalisedText("warnedby"),GetLocalisedText("warndismiss"))
SendWebhookMessage(moderationNotification,string.format(GetLocalisedText("adminwarnedplayer"), getName(src, false, true), getName(id, true, true), reason, WarnedPlayers[id].warns, maxWarnings), "warn", 16711680)
if GetConvar("ea_enableActionHistory", "true") == "true" then
TriggerEvent("EasyAdmin:LogAction", { action = "WARN", discord = CachedPlayers[id].discord, reason = reason, moderator = getName(source, true, false), moderatorId = CachedPlayers[source].discord })
end
if WarnedPlayers[id].warns >= maxWarnings then
if GetConvar("ea_warnAction", "kick") == "kick" then
SendWebhookMessage(moderationNotification,string.format(GetLocalisedText("adminkickedplayer"), getName(src, false, true), getName(id, true, true), reason), "kick", 16711680)
DropPlayer(id, GetLocalisedText("warnkicked"))
WarnedPlayers[id] = nil
if GetConvar("ea_enableActionHistory", "true") == "true" then
TriggerEvent("EasyAdmin:LogAction", {action = "KICK", discord = CachedPlayers[id].discord, reason = "Reached maximum warnings", moderator = "Server", moderatorId = 0})
end
elseif GetConvar("ea_warnAction", "kick") == "ban" then
local bannedIdentifiers = CachedPlayers[id].identifiers or getAllPlayerIdentifiers(id)
local bannedUsername = CachedPlayers[id].name or getName(id, true)
Expand All @@ -784,8 +798,9 @@ Citizen.CreateThread(function()
reason = GetLocalisedText("warnbanned").. string.format(GetLocalisedText("reasonadd"), CachedPlayers[id].name, getName(source, true) )
local ban = {banid = GetFreshBanId(), name = bannedUsername,identifiers = bannedIdentifiers, banner = getName(source, true), reason = reason, expire = expires }
updateBlacklist( ban )


if GetConvar("ea_enableActionHistory", "true") == "true" then
TriggerEvent("EasyAdmin:LogAction", {action = "BAN", discord = CachedPlayers[id].discord, reason = "Reached maximum warnings", moderator = "Server", moderatorId = 0})
end

PrintDebugMessage("Player "..getName(source,true).." warnbanned player "..CachedPlayers[id].name.." for "..reason, 3)
SendWebhookMessage(moderationNotification,string.format(GetLocalisedText("adminbannedplayer"), getName(source, false, true), bannedUsername, reason, formatDateString( expires ), tostring(ban.banid) ), "ban", 16711680)
Expand Down
Loading