Skip to content

Conversation

@DukeOfCheese
Copy link
Contributor

@DukeOfCheese DukeOfCheese commented Apr 15, 2025

  • References player storage system #262 from a couple years ago
  • Tries to keep with EasyAdmin's pattern of using .json files instead of databases
  • Stores moderation actions upon users (e.g warns, kicks, bans and offline bans)
  • Provides name of moderator
  • Attaches IDs to all the actions
  • Provides copy discord button for the staff member / actioned user
  • Delete action button on all actions in history
  • Bans specifically have a designated button to unban from history
  • Automatically clears punishment history after 30 days (convar to change how frequent this occurs)
  • Convar to enable / disable this system
  • Permissions to view actions / delete actions / attaches to ban.remove for the Unban Button
  • Automatically attaches to existing EasyAdmin events for simplicity
  • Currently attaches punishment history by Discord ID (see To-Do)
  • Change how the storage module works
  • Add in webhook logging
  • Edit ban system
  • Locales apart from English (want to ensure everything is complete before adding in locales)

Future Considerations:

  • Add support for the Discord.js bot to view action history

@DukeOfCheese
Copy link
Contributor Author

Very open to suggestions / additions, somewhat in early stages

@DukeOfCheese DukeOfCheese marked this pull request as draft April 15, 2025 11:15
@DukeOfCheese
Copy link
Contributor Author

@Blumlaut Is this use of storage.lua more along the lines of what you were thinking?

@Blumlaut
Copy link
Owner

Blumlaut commented Apr 22, 2025

@Blumlaut Is this use of storage.lua more along the lines of what you were thinking?

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 Storage entity, or do something else...

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

@DukeOfCheese
Copy link
Contributor Author

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

@Blumlaut
Copy link
Owner

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.

@DukeOfCheese
Copy link
Contributor Author

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

@DukeOfCheese DukeOfCheese changed the title Action History (.json use / no dependencies) Storage Module Update + Action History (.json use / no dependencies) Apr 23, 2025
Copy link

Copilot AI left a 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)
Copy link

Copilot AI Nov 16, 2025

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}).

Suggested change
return Storage.getBanIdentifier(theIdentifier)
return Storage.getBanIdentifier({theIdentifier})

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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)
Copy link

Copilot AI Nov 16, 2025

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)
Suggested change
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)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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)
Copy link

Copilot AI Nov 16, 2025

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)
Suggested change
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)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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))
Copy link

Copilot AI Nov 16, 2025

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.

Suggested change
Storage.addAction("BAN", bannedIdentifiers[1], reason, banner or "Unknown", source, expires, formatDateString(expires))
Storage.addAction("BAN", bannedIdentifiers[1], reason, banner or "Unknown", source)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will take a look

Comment on lines 838 to 890
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)
Copy link

Copilot AI Nov 16, 2025

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.

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

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

Copy link
Contributor Author

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)
Copy link

Copilot AI Nov 16, 2025

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)
Suggested change
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)

Copilot uses AI. Check for mistakes.
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

DukeOfCheese and others added 22 commits November 18, 2025 12:27
Co-authored-by: blumlaut <[email protected]>
Co-authored-by: blumlaut <[email protected]>
@DukeOfCheese
Copy link
Contributor Author

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)
Copy link
Owner

@Blumlaut Blumlaut Nov 19, 2025

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:

function GetVersion()
local resourceName = GetCurrentResourceName()
local version = GetResourceMetadata(resourceName, 'version', 0)
local is_master = GetResourceMetadata(resourceName, 'is_master', 0) == "yes" or false

@github-actions github-actions bot added the Stale label Dec 8, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants