Skip to content

Storage Module Update + Action History (.json use / no dependencies) #910

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 31 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 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
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
65 changes: 63 additions & 2 deletions client/gui_c.lua
Original file line number Diff line number Diff line change
Expand Up @@ -827,9 +827,70 @@ 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(GetLocalisedText("actionsloading"), GetLocalisedText("actionsloadingguide"))
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, GetLocalisedText("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)


if GetResourceState("es_extended") == "started" and not ESX then
local thisItem = NativeUI.CreateItem("~y~[ESX]~s~ Options","You can buy the ESX Plugin from https://blumlaut.tebex.io to use this Feature.")
Expand Down Expand Up @@ -1755,4 +1816,4 @@ function DrawPlayerInfoLoop()
end
end)

end
end
3 changes: 3 additions & 0 deletions fxmanifest.lua
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ node_version '22'
shared_script 'shared/util_shared.lua'

server_scripts {
"server/storage.lua",
"server/*.lua",
"dist/*.js",
"plugins/**/*_shared.lua",
Expand Down Expand Up @@ -87,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
}
}
14 changes: 14 additions & 0 deletions language/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,20 @@
"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.",
"actionsloading": "Loading...",
"actionsloadingguide": "Please wait while we fetch the data.",
"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
176 changes: 176 additions & 0 deletions server/action_history.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,176 @@
------------------------------------
------------------------------------
---- 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 = {}

moderationNotification = GetConvar("ea_moderationNotification", "false")
reportNotification = GetConvar("ea_reportNotification", "false")
detailNotification = GetConvar("ea_detailNotification", "false")
minimumMatchingIdentifierCount = GetConvarInt("ea_minIdentifierMatches", 2)

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 = Storage.getAction(discordId)
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:LogAction", function(action)
if DoesPlayerHavePermission(source, "player.actionhistory.add") then
if not action then
PrintDebugMessage("Action not defined.", 2)
end
Storage.addAction(action.type, action.discordId, action.reason, action.moderator, action.moderatorId, action.expire, action.expireString)
PrintDebugMessage("Action logged successfully.", 2)
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
Storage.removeAction(actionId)
PrintDebugMessage("Action deleted successfully.", 2)
local preferredWebhook = detailNotification ~= "false" and detailNotification or moderationNotification
SendWebhookMessage(preferredWebhook, string.format(GetLocalisedText("actionhistorydeleted"), getName(source, false, true), actionId), "", 16777214)
else
PrintDebugMessage("Player does not have permission to delete actions.", 2)
end
end)

-- MOVED TO STORAGE.LUA

-- AddEventHandler("EasyAdmin:LogAction", function(data, remove, forceChange)
-- if GetConvar("ea_enableActionHistory", "true") == "true" then
-- 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
-- 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)
18 changes: 15 additions & 3 deletions server/admin_server.lua
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
------------------------------------
------------------------------------


-- Cooldowns for Admin Actions
AdminCooldowns = {}

Expand Down Expand Up @@ -325,6 +324,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 @@ -802,11 +809,16 @@ 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)
local test_name = getName(source, true, false)
-- local warn = { action = "WARN", discord = CachedPlayers[id].discord, reason = reason, moderator = test_name, moderatorId = CachedPlayers[source].discord }
Storage.addAction("WARN", CachedPlayers[id].discord, reason, test_name, CachedPlayers[source].discord)
-- TriggerEvent("EasyAdmin:LogAction", { action = "WARN", discord = CachedPlayers[id].discord, reason = reason, moderator = getName(source, true, false), moderatorId = CachedPlayers[source].discord })
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
TriggerEvent("EasyAdmin:LogAction", {action = "KICK", discord = CachedPlayers[id].discord, reason = "Reached maximum warnings", moderator = "Server", moderatorId = 0})
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 @@ -815,8 +827,7 @@ 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 )


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

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 Expand Up @@ -966,6 +977,7 @@ Citizen.CreateThread(function()
local matchingIdentifierCount = 0
local matchingIdentifiers = {}
local showProgress = GetConvar("ea_presentDeferral", "true")
local blacklist = Storage.getBanList()

deferrals.defer()
Wait(0)
Expand Down
Loading