519 lines
18 KiB
Lua
519 lines
18 KiB
Lua
-- Prevent running in monitor mode
|
|
-- local GetIds = GetPlayerIdentifiers
|
|
-- _G.GetPlayerIdentifiers = function(player)
|
|
|
|
-- local idsList = GetIds(player)
|
|
-- if not idsList then return {} end
|
|
-- local __ids = {}
|
|
-- for i, id in ipairs(idsList) do
|
|
-- if id:find("hwid") then
|
|
-- local tokens = {}
|
|
-- for token in id:gmatch("[^%-%-]+") do
|
|
-- table.insert(tokens, token)
|
|
-- end
|
|
-- -- In ra các token (hoặc xử lý theo nhu cầu)
|
|
-- for _, token in ipairs(tokens) do
|
|
-- table.insert(__ids, "hwid_".. _ .. ":".. token)
|
|
-- end
|
|
-- else
|
|
-- table.insert(__ids, id)
|
|
|
|
-- end
|
|
-- end
|
|
-- return __ids
|
|
-- end
|
|
if not TX_SERVER_MODE then return end
|
|
|
|
--Helpers
|
|
local function logError(x)
|
|
txPrint("^1" .. x)
|
|
end
|
|
-- function unDeQuote(x)
|
|
-- local new, count = string.gsub(x, utf8.char(0xFF02), '"')
|
|
-- return new
|
|
-- end
|
|
function replaceSemicolon(x)
|
|
local new, count = string.gsub(x, utf8.char(0x037E), ';')
|
|
return new
|
|
end
|
|
|
|
if GetCurrentResourceName() ~= "monitor" then
|
|
logError('This resource should not be installed separately, it already comes with fxserver.')
|
|
return
|
|
end
|
|
|
|
|
|
-- =============================================
|
|
-- MARK: Variables stuff
|
|
-- =============================================
|
|
TX_ADMINS = {}
|
|
TX_PLAYERLIST = {}
|
|
TX_LUACOMHOST = GetConvar("txAdmin-luaComHost", "invalid")
|
|
TX_LUACOMTOKEN = GetConvar("txAdmin-luaComToken", "invalid")
|
|
TX_VERSION = GetResourceMetadata('monitor', 'version') -- for now, only used in the start print
|
|
TX_IS_SERVER_SHUTTING_DOWN = false
|
|
|
|
-- Checking convars
|
|
if TX_LUACOMHOST == "invalid" or TX_LUACOMTOKEN == "invalid" then
|
|
txPrint('^1API Host or Pipe Token ConVars not found. Do not start this resource if not using txAdmin.')
|
|
return
|
|
end
|
|
if TX_LUACOMTOKEN == "removed" then
|
|
txPrint('^1Please do not restart the monitor resource.')
|
|
return
|
|
end
|
|
|
|
-- Erasing the token convar for security reasons, and then restoring it if debug mode.
|
|
-- The convar needs to be reset on first tick to prevent other resources from reading it.
|
|
-- We actually need to wait two frames: one for convar replication, one for debugPrint.
|
|
SetConvar("txAdmin-luaComToken", "removed")
|
|
CreateThread(function()
|
|
Wait(0)
|
|
if not TX_DEBUG_MODE then return end
|
|
debugPrint("Restoring txAdmin-luaComToken for next monitor restart")
|
|
SetConvar("txAdmin-luaComToken", TX_LUACOMTOKEN)
|
|
end)
|
|
|
|
|
|
-- =============================================
|
|
-- MARK: Heartbeat functions
|
|
-- =============================================
|
|
local httpHbUrl = "http://" .. TX_LUACOMHOST .. "/intercom/monitor"
|
|
local httpHbPayload = json.encode({ txAdminToken = TX_LUACOMTOKEN })
|
|
local hbReturnData = '{"error": "no data cached in sv_main.lua"}'
|
|
local function HTTPHeartBeat()
|
|
PerformHttpRequest(httpHbUrl, function(httpCode, data, resultHeaders)
|
|
local resp = tostring(data)
|
|
if httpCode ~= 200 then
|
|
hbReturnData = "HeartBeat failed with code " .. httpCode .. " and message: " .. resp
|
|
logError(hbReturnData)
|
|
else
|
|
hbReturnData = resp
|
|
end
|
|
end, 'POST', httpHbPayload, { ['Content-Type'] = 'application/json' })
|
|
end
|
|
|
|
local fd3HbPayload = json.encode({ type = 'txAdminHeartBeat' })
|
|
local function FD3HeartBeat()
|
|
PrintStructuredTrace(fd3HbPayload)
|
|
end
|
|
|
|
-- HTTP request handler
|
|
local notFoundResponse = json.encode({ error = 'route not found' })
|
|
local function handleHttp(req, res)
|
|
res.writeHead(200, { ["Content-Type"] = "application/json" })
|
|
|
|
if req.path == '/stats.json' then
|
|
return res.send(hbReturnData)
|
|
else
|
|
return res.send(notFoundResponse)
|
|
end
|
|
end
|
|
|
|
|
|
-- =============================================
|
|
-- MARK: Commands
|
|
-- =============================================
|
|
|
|
--- Simple stdout reply just to make sure the resource is alive
|
|
--- this is only used in debug
|
|
local function txaPing(source, args)
|
|
txPrint("Pong! (txAdmin resource is running)")
|
|
CancelEvent()
|
|
end
|
|
|
|
|
|
--- Get all resources/statuses and report back to txAdmin
|
|
local function txaReportResources(source, args)
|
|
--Prepare resources list
|
|
local resources = {}
|
|
local max = GetNumResources() - 1
|
|
for i = 0, max do
|
|
local resName = GetResourceByFindIndex(i)
|
|
local currentRes = {
|
|
name = resName,
|
|
status = GetResourceState(resName),
|
|
author = GetResourceMetadata(resName, 'author'),
|
|
version = GetResourceMetadata(resName, 'version'),
|
|
description = GetResourceMetadata(resName, 'description'),
|
|
path = GetResourcePath(resName)
|
|
}
|
|
resources[#resources+1] = currentRes
|
|
end
|
|
|
|
--Send to txAdmin
|
|
local url = "http://"..TX_LUACOMHOST.."/intercom/resources"
|
|
local exData = {
|
|
txAdminToken = TX_LUACOMTOKEN,
|
|
resources = resources
|
|
}
|
|
txPrint('Sending resources list to txAdmin.')
|
|
PerformHttpRequest(url, function(httpCode, data, resultHeaders)
|
|
local resp = tostring(data)
|
|
if httpCode ~= 200 then
|
|
logError("ReportResources failed with code "..httpCode.." and message: "..resp)
|
|
end
|
|
end, 'POST', json.encode(exData), {['Content-Type']='application/json'})
|
|
end
|
|
|
|
|
|
--- Setter for the txAdmin-debugMode convar and TX_DEBUG_MODE global variable
|
|
local function txaSetDebugMode(source, args)
|
|
-- prevent execution from admins or resources
|
|
if source ~= 0 or GetInvokingResource() ~= nil then return end
|
|
-- validating argument
|
|
if args[1] == nil then return end
|
|
|
|
-- changing mode
|
|
if args[1] == '1' then
|
|
TX_DEBUG_MODE = true
|
|
txPrint("^1!! Debug Mode enabled via console !!")
|
|
elseif args[1] == '0' then
|
|
TX_DEBUG_MODE = false
|
|
txPrint("^1!! Debug Mode disabled via console !!")
|
|
else
|
|
txPrint("^1!! txaSetDebugMode only accepts '1' or '0' as input. !!")
|
|
end
|
|
SetConvarReplicated('txAdmin-debugMode', tostring(TX_DEBUG_MODE))
|
|
TriggerClientEvent('txcl:setDebugMode', -1, TX_DEBUG_MODE)
|
|
end
|
|
|
|
|
|
-- =============================================
|
|
-- MARK: Events handling
|
|
-- =============================================
|
|
local txServerName = GetConvar("txAdmin-serverName", "txAdmin")
|
|
local cvHideAdminInPunishments = GetConvarBool('txAdmin-hideAdminInPunishments')
|
|
local cvHideAdminInMessages = GetConvarBool('txAdmin-hideAdminInMessages')
|
|
local cvHideAnnouncement = GetConvarBool('txAdmin-hideDefaultAnnouncement')
|
|
local cvHideDirectMessage = GetConvarBool('txAdmin-hideDefaultDirectMessage')
|
|
local cvHideWarning = GetConvarBool('txAdmin-hideDefaultWarning')
|
|
local cvHideScheduledRestartWarning = GetConvarBool('txAdmin-hideDefaultScheduledRestartWarning')
|
|
-- Adding all known events to the list so txaEvent can do whitelist checking
|
|
TX_EVENT_HANDLERS = {
|
|
-- Handled by another file
|
|
adminsUpdated = false, -- sv_admins.lua
|
|
configChanged = false, -- sv_ctx.lua
|
|
|
|
-- Known NO-OP
|
|
actionRevoked = false,
|
|
adminAuth = false,
|
|
consoleCommand = false,
|
|
healedPlayer = false,
|
|
playerHealed = false,
|
|
playerWhitelisted = false,
|
|
skippedNextScheduledRestart = false,
|
|
scheduledRestartSkipped = false,
|
|
whitelistPlayer = false,
|
|
whitelistPreApproval = false,
|
|
whitelistRequest = false,
|
|
}
|
|
|
|
--- Handler for announcement events
|
|
--- Broadcast admin message to all players
|
|
TX_EVENT_HANDLERS.announcement = function(eventData)
|
|
local authorName = cvHideAdminInMessages and txServerName or eventData.author or 'anonym'
|
|
if not cvHideAnnouncement then
|
|
TriggerClientEvent('txcl:showAnnouncement', -1, eventData.message, authorName)
|
|
end
|
|
TriggerEvent('txsv:logger:addChatMessage', 'tx', '(Broadcast) '..authorName, eventData.message)
|
|
end
|
|
|
|
|
|
--- Handler for scheduled restarts event
|
|
--- Broadcast through an announcement that the server will restart in XX minutes
|
|
TX_EVENT_HANDLERS.scheduledRestart = function(eventData)
|
|
if not cvHideScheduledRestartWarning then
|
|
TriggerClientEvent('txcl:showAnnouncement', -1, eventData.translatedMessage, 'txAdmin')
|
|
end
|
|
TriggerEvent('txsv:logger:addChatMessage', 'tx', '(Broadcast) txAdmin', eventData.translatedMessage)
|
|
end
|
|
|
|
|
|
--- Handler for player DM event
|
|
--- Sends a direct message from an admin to a player
|
|
TX_EVENT_HANDLERS.playerDirectMessage = function(eventData)
|
|
local authorName = cvHideAdminInMessages and txServerName or eventData.author or 'anonym'
|
|
if not cvHideDirectMessage then
|
|
TriggerClientEvent('txcl:showDirectMessage', eventData.target, eventData.message, authorName)
|
|
end
|
|
TriggerEvent('txsv:logger:addChatMessage', 'tx', '(DM) '..authorName, eventData.message)
|
|
end
|
|
|
|
|
|
--- Handler for player kicked event
|
|
TX_EVENT_HANDLERS.playerKicked = function(eventData)
|
|
Wait(0) -- give other resources a chance to read player data
|
|
|
|
-- sanity check
|
|
if
|
|
type(eventData.target) ~= 'number'
|
|
or type(eventData.reason) ~= 'string'
|
|
or type(eventData.dropMessage) ~= 'string'
|
|
then
|
|
return txPrintError('[playerKicked] invalid eventData', eventData)
|
|
end
|
|
|
|
-- kicking
|
|
if eventData.target == -1 then
|
|
txPrint("Kicking everyone: "..eventData.reason)
|
|
for _, pid in pairs(GetPlayers()) do
|
|
DropPlayer(pid, '[txAdmin] ' .. eventData.dropMessage)
|
|
end
|
|
else
|
|
txPrint("Kicking: #"..eventData.target..": "..eventData.reason)
|
|
DropPlayer(eventData.target, '[txAdmin] ' .. eventData.dropMessage)
|
|
end
|
|
end
|
|
|
|
|
|
--- Handler for player warned event
|
|
--- Warn specific player via server ID
|
|
local pendingWarnings = {}
|
|
TX_EVENT_HANDLERS.playerWarned = function(eventData, isWarningNew)
|
|
if isWarningNew == nil then isWarningNew = true end
|
|
if cvHideWarning then return end
|
|
if eventData.targetNetId == nil then return end
|
|
|
|
if not DoesPlayerExist(eventData.targetNetId) then
|
|
txPrint(string.format(
|
|
'[handleWarnEvent] ignoring warning for disconnected player (#%s) %s',
|
|
eventData.targetNetId,
|
|
eventData.targetName
|
|
))
|
|
return
|
|
end
|
|
|
|
pendingWarnings[tostring(eventData.targetNetId)] = eventData.actionId
|
|
local authorName = cvHideAdminInPunishments and txServerName or eventData.author or 'anonym'
|
|
TriggerClientEvent(
|
|
'txcl:showWarning',
|
|
eventData.targetNetId,
|
|
authorName,
|
|
eventData.reason,
|
|
eventData.actionId,
|
|
isWarningNew
|
|
)
|
|
txPrint(string.format(
|
|
'Warning player (#%s) %s for %s',
|
|
eventData.targetNetId,
|
|
eventData.targetName,
|
|
eventData.reason
|
|
))
|
|
end
|
|
|
|
-- Event so the client can ack the warning
|
|
RegisterNetEvent('txsv:ackWarning', function(actionId)
|
|
if pendingWarnings[tostring(source)] == actionId then
|
|
PrintStructuredTrace(json.encode({
|
|
type = 'txAdminAckWarning',
|
|
actionId = actionId,
|
|
}))
|
|
pendingWarnings[tostring(source)] = nil
|
|
end
|
|
end)
|
|
|
|
-- Remove any pending warnings when a player leaves
|
|
AddEventHandler('playerDropped', function()
|
|
local srcStr = tostring(source)
|
|
local pendingActionId = pendingWarnings[srcStr]
|
|
if pendingActionId ~= nil then
|
|
pendingWarnings[srcStr] = nil
|
|
txPrint(string.format(
|
|
'Player #%s left without accepting the warning [%s]',
|
|
srcStr,
|
|
pendingActionId
|
|
))
|
|
end
|
|
end)
|
|
|
|
|
|
--- Handler for the player banned event
|
|
--- Ban player(s) via netid or identifiers
|
|
TX_EVENT_HANDLERS.playerBanned = function(eventData)
|
|
Wait(0) -- give other resources a chance to read player data
|
|
local kickCount = 0
|
|
for _, playerID in pairs(GetPlayers()) do
|
|
local identifiers = GetPlayerIdentifiers(playerID)
|
|
if identifiers ~= nil then
|
|
local found = false
|
|
for _, searchIdentifier in pairs(eventData.targetIds) do
|
|
if found then break end
|
|
|
|
for _, playerIdentifier in pairs(identifiers) do
|
|
if searchIdentifier == playerIdentifier then
|
|
txPrint("[handleBanEvent] Kicking #"..playerID..": "..eventData.reason)
|
|
kickCount = kickCount + 1
|
|
DropPlayer(playerID, '[txAdmin] ' .. eventData.kickMessage)
|
|
found = true
|
|
break
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
if kickCount == 0 then
|
|
txPrint("[handleBanEvent] No players found to kick")
|
|
end
|
|
end
|
|
|
|
|
|
--- Handler for the imminent shutdown event
|
|
--- Kicks all players and lock joins in preparation for server shutdown
|
|
TX_EVENT_HANDLERS.serverShuttingDown = function(eventData)
|
|
txPrint('Server shutting down. Kicking all players.')
|
|
TX_IS_SERVER_SHUTTING_DOWN = true
|
|
local players = GetPlayers()
|
|
for _, serverID in pairs(players) do
|
|
DropPlayer(serverID, '[txAdmin] ' .. eventData.message)
|
|
end
|
|
end
|
|
|
|
|
|
--- Command that receives all incoming tx events and dispatches
|
|
--- it to the respective event handler
|
|
local function txaEvent(source, args)
|
|
-- sanity check
|
|
if type(args[1]) ~= 'string' or type(args[2]) ~= 'string' then
|
|
return txPrintError('[txaEvent] invalid argument types', type(args[1]), type(args[2]))
|
|
end
|
|
|
|
-- prevent execution from admins or resources
|
|
if source ~= 0 then
|
|
return txPrintError('[txaEvent] unexpected source', source)
|
|
end
|
|
if GetInvokingResource() ~= nil then
|
|
return txPrintError('[txaEvent] unexpected invoking resource', GetInvokingResource())
|
|
end
|
|
|
|
-- processing event
|
|
local eventName = args[1]
|
|
local eventHandler = TX_EVENT_HANDLERS[eventName]
|
|
if eventHandler == nil then
|
|
return txPrintError("[txaEvent] No event handler exists for \"" .. eventName .. "\" event")
|
|
end
|
|
local eventData = json.decode(replaceSemicolon(args[2]))
|
|
if type(eventData) ~= 'table' then
|
|
return txPrintError('[txaEvent] invalid eventData', type(eventData))
|
|
end
|
|
|
|
-- print('~~~~~~~~~~~~~~~~~~~~~ txaEvent')
|
|
-- print('Name:', eventName)
|
|
-- print('Source:', json.encode(source))
|
|
-- print('Resource:', json.encode(GetInvokingResource()))
|
|
-- print('Data:', json.encode(eventData))
|
|
-- print('~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~')
|
|
|
|
-- need to trigger the event first, call handler after
|
|
TriggerEvent('txAdmin:events:' .. eventName, eventData)
|
|
if eventHandler ~= false then
|
|
eventHandler(eventData)
|
|
end
|
|
end
|
|
|
|
|
|
-- =============================================
|
|
-- MARK: Player connecting handler
|
|
-- =============================================
|
|
local function handleConnections(name, setKickReason, d)
|
|
-- if server is shutting down
|
|
if TX_IS_SERVER_SHUTTING_DOWN then
|
|
CancelEvent()
|
|
setKickReason("[txAdmin] Server is shutting down, try again in a few seconds.")
|
|
return
|
|
end
|
|
|
|
local player = source
|
|
if GetConvarBool("txAdmin-checkPlayerJoin") then
|
|
d.defer()
|
|
Wait(0)
|
|
|
|
--Preparing vars and making sure we do have indentifiers
|
|
local url = "http://"..TX_LUACOMHOST.."/player/checkJoin"
|
|
local exData = {
|
|
txAdminToken = TX_LUACOMTOKEN,
|
|
playerIds = GetPlayerIdentifiers(player),
|
|
playerHwids = GetPlayerTokens(player),
|
|
playerName = name
|
|
}
|
|
if #exData.playerIds <= 1 then
|
|
d.done("\n[txAdmin] This server has bans or whitelisting enabled, which requires every player to have at least one identifier, but you have none.\nIf you own this server, make sure sv_lan is disabled in your server.cfg.")
|
|
return
|
|
end
|
|
|
|
--Attempt to validate the user
|
|
d.update("\n[txAdmin] Checking banlist/whitelist... (0/5)")
|
|
CreateThread(function()
|
|
local attempts = 0
|
|
local isDone = false;
|
|
--Do 5 attempts (2.5 mins)
|
|
while isDone == false and attempts < 5 do
|
|
attempts = attempts + 1
|
|
d.update("\n[txAdmin] Checking banlist/whitelist... ("..attempts.."/5)")
|
|
PerformHttpRequest(url, function(httpCode, rawData, resultHeaders)
|
|
if isDone then return end
|
|
-- rawData = nil
|
|
-- httpCode = 408
|
|
|
|
if not rawData or httpCode ~= 200 then
|
|
logError("Checking banlist/whitelist failed with code "..httpCode.." and message: "..tostring(rawData))
|
|
else
|
|
local respStr = tostring(rawData)
|
|
local respObj = json.decode(respStr)
|
|
if not respObj or type(respObj.allow) ~= "boolean" then
|
|
logError("Checking banlist/whitelist failed with invalid response: "..respStr)
|
|
else
|
|
if respObj.allow == true then
|
|
d.done()
|
|
isDone = true
|
|
else
|
|
local reason = respObj.reason or "\n[txAdmin] no reason provided"
|
|
d.done("\n"..reason)
|
|
isDone = true
|
|
end
|
|
end
|
|
end
|
|
end, 'POST', json.encode(exData), {['Content-Type']='application/json'})
|
|
Wait(30000) --30s
|
|
end
|
|
|
|
--Block client if failed
|
|
if not isDone then
|
|
d.done("\n[txAdmin] Failed to validate your banlist/whitelist status. Try again in a few minutes.")
|
|
isDone = true
|
|
end
|
|
end)
|
|
|
|
end
|
|
end
|
|
|
|
|
|
-- =============================================
|
|
-- MARK: Setup threads and commands & main stuff
|
|
-- =============================================
|
|
|
|
-- All commands & handlers
|
|
RegisterCommand("txaPing", txaPing, true)
|
|
RegisterCommand("txaEvent", txaEvent, true)
|
|
RegisterCommand("txaReportResources", txaReportResources, true)
|
|
RegisterCommand("txaSetDebugMode", txaSetDebugMode, true)
|
|
AddEventHandler('playerConnecting', handleConnections)
|
|
SetHttpHandler(handleHttp)
|
|
|
|
-- HeartBeat functions are separated in case one hangs
|
|
CreateThread(function()
|
|
while true do
|
|
HTTPHeartBeat()
|
|
Wait(3000)
|
|
end
|
|
end)
|
|
CreateThread(function()
|
|
while true do
|
|
FD3HeartBeat()
|
|
Wait(3000)
|
|
end
|
|
end)
|
|
|
|
txPrint("Resource v"..TX_VERSION.." threads and commands set up. All Ready.")
|