-
Notifications
You must be signed in to change notification settings - Fork 166
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
base: master
Are you sure you want to change the base?
Conversation
… dependencies using a .json file
…story display format
…(BAN, KICK, WARN, UNBAN) across the action history
|
Very open to suggestions / additions, somewhat in early stages |
…ntly, regardless of action history setting
…ved clarity and performance
|
@Blumlaut Is this use of storage.lua more along the lines of what you were thinking? |
…functions for improved organization
i'll have to review the code in detail when i have more time but glancing over it thats about what i was thinking of, i'm just wondering how to best make this work with plugins, i wonder if we should just let them overwrite the Edit: Maybe instead of letting them overwrite the storage entity introduce something like with plugins and have a storage backend convar that people can set, this would also allow us to merge additional storage backends without having to overwrite the default |
|
Would it be worth removing the custom banlist convar in favour of this updated storage system or should the function still check for it for backwards compatibility |
Good question, it hasn't been officially supported (or documented..) for years now, i'm not sure if it works any more even. I'd probably drop it altogether and force people to migrate to the new system. |
…ction history management
…methods and action logging
|
Ban system completely overhauled with new system, commented at points throughout banlist.lua just to make my ideas clear. At the moment, no old code is deleted, rather commented out to ensure nothing is missed. Small thing with this storage module, the performBanlistUpgrades() might not work as expected, I'll have to try it out once I get back to my PC |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Pull Request Overview
This pull request introduces a comprehensive action history tracking system for EasyAdmin that stores moderation actions (warns, kicks, bans, offline bans) in JSON files without external dependencies. The system integrates with existing EasyAdmin events and provides a GUI for viewing and managing action history.
Key Changes:
- New storage module (
storage.lua) with a unified API for managing banlists and action history using versioned JSON files - Action history tracking system with Discord ID-based user identification, automatic 30-day expiry, and webhook logging
- Client GUI enhancements to view, delete, and unban from action history with permission controls
Reviewed Changes
Copilot reviewed 14 out of 14 changed files in this pull request and generated 21 comments.
Show a summary per file
| File | Description |
|---|---|
| server/storage.lua | New unified storage module providing API functions for banlist and action history management with versioned JSON file format |
| server/action_history.lua | New action history system with server events for viewing, logging, and deleting moderation actions |
| server/banlist.lua | Refactored to use new Storage API instead of direct file operations, with action history integration |
| server/admin_server.lua | Added action history logging for kicks and warns, integrated with existing moderation events |
| client/gui_c.lua | Added action history submenu with view, delete, and unban functionality in the player menu |
| shared/util_shared.lua | Added three new permissions for action history: view, add, and delete |
| fxmanifest.lua | Added storage.lua to server scripts and two new convars for action history configuration |
| language/*.json | Added localization strings for action history UI elements across 7 languages |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| end | ||
| end | ||
| return identifierfound | ||
| return Storage.getBanIdentifier(theIdentifier) |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The IsIdentifierBanned function calls Storage.getBanIdentifier(theIdentifier) which expects an array of identifiers (see storage.lua line 64-75 where it loops over identifiers), but passes a single string identifier instead. This will cause the function to fail. Wrap the identifier in a table: Storage.getBanIdentifier({theIdentifier}).
| return Storage.getBanIdentifier(theIdentifier) | |
| return Storage.getBanIdentifier({theIdentifier}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function returns a boolean. Don't think this is necessary
| PrintDebugMessage("banlist.json file was missing, we created a new one.", 2) | ||
| content = json.encode({}) | ||
| end | ||
| local saved = SaveResourceFile(GetCurrentResourceName(), "banlist.json", json.encode(banlist, {indent = true}), -1) |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The removeBan function saves the banlist incorrectly. It should save a structured object with version and data fields (consistent with LoadList), but it's only saving the array directly.
Change line 125 to:
local saved = SaveResourceFile(GetCurrentResourceName(), "banlist.json", json.encode({version = currentVersion, data = banlist}, {indent = true}), -1)| local saved = SaveResourceFile(GetCurrentResourceName(), "banlist.json", json.encode(banlist, {indent = true}), -1) | |
| local saved = SaveResourceFile(GetCurrentResourceName(), "banlist.json", json.encode({version = currentVersion, data = banlist}, {indent = true}), -1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Banlist is set as a variable at the top of the file and does not need to be edited here
| PrintDebugMessage("actions.json file was missing, we created a new one.", 2) | ||
| content = json.encode({}) | ||
| end | ||
| local saved = SaveResourceFile(GetCurrentResourceName(), "actions.json", json.encode(actions, {indent = true}), -1) |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The addAction function saves the actions incorrectly. It should save a structured object with version and data fields (consistent with LoadList), but it's only saving the array directly.
Change line 184 to:
local saved = SaveResourceFile(GetCurrentResourceName(), "actions.json", json.encode({version = currentVersion, data = actions}, {indent = true}), -1)| local saved = SaveResourceFile(GetCurrentResourceName(), "actions.json", json.encode(actions, {indent = true}), -1) | |
| local saved = SaveResourceFile(GetCurrentResourceName(), "actions.json", json.encode({version = currentVersion, data = actions}, {indent = true}), -1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above
| -- local ban = {banid = GetFreshBanId(), name = bannedUsername,identifiers = bannedIdentifiers, banner = banner or "Unknown", reason = reason, expire = expires, expireString = formatDateString(expires) } | ||
| -- updateBlacklist( ban ) | ||
| Storage.addBan(GetFreshBanId(), bannedUsername, bannedIdentifiers, banner or "Unknown", reason, expires, formatDateString(expires), "BAN", os.time()) | ||
| Storage.addAction("BAN", bannedIdentifiers[1], reason, banner or "Unknown", source, expires, formatDateString(expires)) |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The function signature for Storage.addAction expects 5 required parameters (type, identifier, reason, moderator_name, moderator_identifier), but this call passes 7 arguments including expires and formatDateString(expires). These extra parameters don't match the function definition in storage.lua line 169 and will be ignored or cause unexpected behavior.
| Storage.addAction("BAN", bannedIdentifiers[1], reason, banner or "Unknown", source, expires, formatDateString(expires)) | |
| Storage.addAction("BAN", bannedIdentifiers[1], reason, banner or "Unknown", source) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Will take a look
| 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) |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The event handler for EasyAdmin:ReceiveActionHistory is registered inside the menu generation loop, which means it will be registered multiple times if the menu is regenerated or if there are multiple players. This creates duplicate handlers that will all execute, causing the action history menu to be populated multiple times. Register the event handler once outside the menu generation function or use a unique handler registration pattern.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I want it to refire everytime you open menu in case new actions come through so I believe this is intended
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is TECHNICALLY correct, but the menu is cleared before
| bans = {} | ||
| }) | ||
| end | ||
| local saved = SaveResourceFile(GetCurrentResourceName(), "banlist.json", json.encode(banlist, {indent = true}), -1) |
Copilot
AI
Nov 16, 2025
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The addBan function saves the banlist incorrectly. It should save a structured object with version and data fields (as loaded by LoadList), but it's only saving the array directly. This breaks consistency with the file format.
Change line 97 to:
local saved = SaveResourceFile(GetCurrentResourceName(), "banlist.json", json.encode({version = currentVersion, data = banlist}, {indent = true}), -1)| local saved = SaveResourceFile(GetCurrentResourceName(), "banlist.json", json.encode(banlist, {indent = true}), -1) | |
| local saved = SaveResourceFile(GetCurrentResourceName(), "banlist.json", json.encode({version = currentVersion, data = banlist}, {indent = true}), -1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as above
Co-authored-by: blumlaut <[email protected]>
Co-authored-by: blumlaut <[email protected]>
Co-authored-by: blumlaut <[email protected]>
Co-authored-by: blumlaut <[email protected]>
…gui_c.lua banlist menu item)
…ove expired actions
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Copilot <[email protected]>
|
I've also gone through all of Copilot's suggestions, some of it was beneficial, the unresolved ones are ones I'm not 100% sure about whether it would impact |
|
|
||
| local function LoadList(fileName) | ||
| local content = LoadResourceFile(GetCurrentResourceName(), fileName .. ".json") | ||
| local currentVersion = GetConvar("$ea_storageAPIVersion", 1) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
this is not quite what i meant.
I was more thinking of
https://docs.fivem.net/natives/?_0x964BAB1D
see:
EasyAdmin/shared/util_shared.lua
Lines 195 to 198 in b81a223
| function GetVersion() | |
| local resourceName = GetCurrentResourceName() | |
| local version = GetResourceMetadata(resourceName, 'version', 0) | |
| local is_master = GetResourceMetadata(resourceName, 'is_master', 0) == "yes" or false |
Future Considerations: