Framework, Utils & More
Quality of life scripts for your FiveM server. Performance optimized resources that install in minutes, with free updates and support that actually answers.
bridge/framework/server.lua
lua
local function getPhoneNumber(identifier)
local result = MySQL.query.await('SELECT phone_number FROM users WHERE identifier = ?', { identifier })
return result and result[1].phone_number
end
---Get Player Info
---@param playerId string | number | nil
---@return table | nil
function GetPlayerInfo(playerId)
if not playerId then return nil end
if GetResourceState('qbx_core') == 'started' then
local player = (type(playerId) == 'string' and (exports.qbx_core:GetPlayerByCitizenId(playerId) or exports.qbx_core:GetOfflinePlayer(playerId))) or exports.qbx_core:GetPlayer(playerId)
if not player then assert(player, "Player not found.") return nil end
return {
citizenId = player.PlayerData.citizenid,
fullName = player.PlayerData.charinfo.firstname .. ' ' .. player.PlayerData.charinfo.lastname,
phoneNumber = player.PlayerData.charinfo.phone,
callsign = player.PlayerData.metadata.callsign or 'N/A',
job = {
name = player.PlayerData.job.name or 'N/A',
grade = player.PlayerData.job.grade.level or 1
},
addMoney = function(type, amount, reason)
return player.Functions.AddMoney(type, amount, reason)
end,
removeMoney = function(type, amount, reason)
return player.Functions.RemoveMoney(type, amount, reason)
end
}
elseif GetResourceState('qb-core') == 'started' then
local QBCore = exports['qb-core']:GetCoreObject()
local player = (type(playerId) == 'string' and (QBCore.Functions.GetPlayerByCitizenId(playerId) or QBCore.Functions.GetOfflinePlayerByCitizenId(playerId))) or QBCore.Functions.GetPlayer(playerId)
if not player then assert(player, "Player not found.") return nil end
return {
citizenId = player.PlayerData.citizenid,
fullName = player.PlayerData.charinfo.firstname .. ' ' .. player.PlayerData.charinfo.lastname,
phoneNumber = player.PlayerData.charinfo.phone,
callsign = player.PlayerData.metadata.callsign or 'N/A',
job = {
name = player.PlayerData.job.name or 'N/A',
grade = player.PlayerData.job.grade.level or 1
},
addMoney = function(type, amount, reason)
return player.Functions.AddMoney(type, amount, reason)
end,
removeMoney = function(type, amount, reason)
return player.Functions.RemoveMoney(type, amount, reason)
end
}
elseif GetResourceState('es_extended') == 'started' then
local ESX = exports.es_extended:getSharedObject()
local player = type(playerId) == 'string' and ESX.GetPlayerFromIdentifier(playerId) or ESX.GetPlayerFromId(playerId)
if not player then print("Player not found.") return nil end
return {
citizenId = player.identifier,
fullName = player.name,
phoneNumber = getPhoneNumber(player.identifier),
callsign = player.callsign or 'N/A', -- Not sure about this, check and replace this with your callsign function/export
job = {
name = player.getJob().name or 'N/A',
grade = player.getJob().grade or 1
},
addMoney = function(type, amount, reason)
if type == 'cash' then type = 'money' end
player.addAccountMoney(type, amount, reason)
return true
end,
removeMoney = function(type, amount, reason)
if type == 'cash' then type = 'money' end
player.removeAccountMoney(type, amount, reason)
return true
end
}
end
return nil
end
---Get Player Info
---@param plateNumber string | number | nil
---@return table | nil
function GetPlayerInfoFromPlate(plateNumber)
if not plateNumber then assert(plateNumber, "Plate number not found to get player info") return nil end
if GetResourceState('qb-core') == 'started' or GetResourceState('qbx_core') == 'started' then
local result = MySQL.single.await([[
SELECT
players.citizenid
FROM player_vehicles
LEFT JOIN players ON players.citizenid = player_vehicles.citizenid
WHERE plate = ? OR fakeplate = ? LIMIT 1
]], { plateNumber, plateNumber })
assert(result, "Player not found by plate number.")
return GetPlayerInfo(result.citizenid)
elseif GetResourceState('es_extended') == 'started' then
local result = MySQL.single.await([[
SELECT
users.identifier
FROM owned_vehicles
LEFT JOIN users ON users.identifier = owned_vehicles.identifier
WHERE plate = ? LIMIT 1
]], { plateNumber })
assert(result, "Player not found by plate number.")
return GetPlayerInfo(result.identifier)
end
return nil
end
---Get Vehicle Name
---@param vehicleId number | integer
---@return string
function GetVehicleName(vehicleId)
if not vehicleId then return nil, nil end
local model = GetEntityModel(vehicleId)
if GetResourceState('qbx_core') == 'started' then
local vehicle = exports.qbx_core:GetVehiclesByHash(model)
return (vehicle.brand and (vehicle.brand .. ' ' .. vehicle.name)) or vehicle.name
elseif GetResourceState('qb-core') == 'started' then
local QBCore = exports['qb-core']:GetCoreObject()
if QBCore.Shared.VehicleHashes then
local vehicle = QBCore.Shared.VehicleHashes[model]
return (vehicle.brand and (vehicle.brand .. ' ' .. vehicle.name)) or vehicle.name
else
return GetVehicleNumberPlateText(vehicleId)
end
else
return GetVehicleNumberPlateText(vehicleId)
end
endbridge/inventory/client.lua
lua
---Display metadata for the citation item, only for ox_inventory
CreateThread(function()
if GetResourceState('ox_inventory') == 'missing' then
return
end
while true do
Wait(1000)
if GetResourceState('ox_inventory') == 'started' then
exports.ox_inventory:displayMetadata({
fullName = 'Name',
citizenId = 'Citizen ID',
fineAmount = 'Fine Amount',
-- violations = 'Violations',
-- vehicleModel = 'Vehicle Model',
-- plateNumber = 'Plate Number',
-- currentDate = 'Current Date',
-- streetName = 'Street Name',
-- officerCitizenId = 'AX12344',
-- officerName = 'Officer Name',
-- officerCallsign = 'Officer Callsign',
-- comments = 'Comments',
-- evidence = 'Evidence'
})
break
end
end
end)
bridge/inventory/server.lua
lua
---Can Carry an item
---@param source number | nil
---@param item string
---@param amount number
function CanCarryItem(source, item, amount)
if GetResourceState('ox_inventory') == 'started' then
return exports.ox_inventory:CanCarryItem(source, item, amount)
elseif GetResourceState('qb-inventory') == 'started' then
return exports['qb-inventory']:CanAddItem(source, item, amount)
elseif GetResourceState('ps-inventory') == 'started' then
return exports['ps-inventory']:CanAddItem(source, item, amount)
else
assert(false, "No inventory resource found.")
return false
end
end
---Get item by slot number
---@param source number | nil
---@param slot number
function GetItemBySlot(source, slot)
if GetResourceState('ox_inventory') == 'started' then
return exports.ox_inventory:GetSlot(source, slot)
elseif GetResourceState('qb-inventory') == 'started' then
return exports['qb-inventory']:GetItemBySlot(source, slot)
elseif GetResourceState('ps-inventory') == 'started' then
return exports['ps-inventory']:GetItemBySlot(source, slot)
else
assert(false, "No inventory resource found.")
return nil
end
end
---Add item to inventory
---@param source number | nil
---@param item string
---@param amount number
---@param metadata table<any>
function AddItem(source, item, amount, metadata)
if GetResourceState('ox_inventory') == 'started' then
return exports.ox_inventory:AddItem(source, item, amount, metadata)
elseif GetResourceState('qb-inventory') == 'started' then
return exports['qb-inventory']:AddItem(source, item, amount, nil, metadata)
elseif GetResourceState('ps-inventory') == 'started' then
return exports['ps-inventory']:AddItem(source, item, amount, nil, metadata)
else
assert(false, "No inventory resource found.")
return false
end
end
---Remove item from inventory
---@param source number | nil
---@param item string
---@param amount number
---@param metadata table<any>
function RemoveItem(source, item, amount, metadata)
if GetResourceState('ox_inventory') == 'started' then
return exports.ox_inventory:AddItem(source, item, amount, metadata)
elseif GetResourceState('qb-inventory') == 'started' then
return exports['qb-inventory']:AddItem(source, item, amount, nil, metadata)
elseif GetResourceState('ps-inventory') == 'started' then
return exports['ps-inventory']:AddItem(source, item, amount, nil, metadata)
else
assert(false, "No inventory resource found.")
return false
end
end
---Handles Item Usage
CreateThread(function()
if GetResourceState('qb-inventory') ~= 'missing' then
local timeout = 10
while not GetResourceState('qb-inventory') ~= 'started' and timeout > 0 do
timeout = timeout - 1
Wait(1000)
end
local QBCore = exports['qb-core']:GetCoreObject()
for _, itemName in pairs(Config.Items) do
QBCore.Functions.CreateUseableItem(itemName, function(source, item)
if item.name == Config.Items.show_receipt then
exports['cad-citations']:showCitation(source, item.metadata)
elseif item.name == Config.Items.create_citation then
local citation = GetAutomatedInfo(source)
exports['cad-citations']:createCitation(source, citation)
end
end)
end
end
if GetResourceState('ps-inventory') ~= 'missing' then
local timeout = 10
while not GetResourceState('ps-inventory') ~= 'started' and timeout > 0 do
timeout = timeout - 1
Wait(1000)
end
for _, itemName in pairs(Config.Items) do
exports['ps-inventory']:CreateUsableItem(itemName, function(source, item)
if item.name == Config.Items.show_receipt then
exports['cad-citations']:showCitation(source, item.metadata)
elseif item.name == Config.Items.create_citation then
local citation = GetAutomatedInfo(source)
exports['cad-citations']:createCitation(source, citation)
end
end)
end
end
end)
---This function is used to handle item usage, specifically for ox_inventory.
---@param event string
---@param item table<any>
---@param inv table<any>
---@param slot number
---@param data table<any>
exports('useItem', function(event, item, inv, slot, data)
if event == 'usingItem' then
if item.name == Config.Items.show_receipt then
local _item = GetItemBySlot(inv.id, slot)
assert(_item, "Item data not found for slot: " .. tostring(slot))
exports['cad-citations']:showCitation(inv.id, _item.metadata)
return false
end
if item.name == Config.Items.create_citation then
local citation = GetAutomatedInfo(inv.id)
exports['cad-citations']:createCitation(inv.id, citation)
return false
end
return
end
end)bridge/utils/client.lua
lua
---Take Photo
---@param data { method: string, authKey: string } Upload Config
---@param cb function Callback function returning the captured image url
function TakeScreenPhoto(data, cb)
if data.method == 'discord' then
exports['screenshot-basic']:requestScreenshotUpload(data.authKey, 'files[]', function(response)
local image = json.decode(response)
if not data or not image then return cb('') end
cb((image.attachments[1] and image.attachments[1]?.proxy_url) or 'Invalid Url')
end)
elseif data.method == 'fivemanage' then
exports['screenshot-basic']:requestScreenshotUpload('https://fmapi.net/api/v2/image', 'file', {
headers = { Authorization = data.authKey },
encoding = 'png'
}, function(response)
local resp = json.decode(response)
cb((resp and resp?.data and resp.data?.url) or '')
end)
elseif data.method == 'fivemerr' then
exports['screenshot-basic']:requestScreenshotUpload('https://api.fivemerr.com/v1/media/images', 'file', {
headers = { Authorization = data.authKey },
encoding = 'png'
}, function(response)
local resp = json.decode(response)
cb((resp and resp.url) or '')
end)
end
end
---Notification
---@param data { title: string, description: string, type?: string, duration?: number }
RegisterNetEvent('cad-citations:notify', function(data)
if not data then return end
lib.notify({
title = data.title,
description = data.description,
type = data.type,
duration = 5000
})
end)
---On use event listener
---@param type string 'new' | 'existing'
RegisterNetEvent('cad-citations:onUse', function(type)
local emote = (type == 'existing') and 'clipboard' or 'notepad'
if GetResourceState('scully_emotemenu') == 'started' then
exports.scully_emotemenu:PlayEmoteByCommand(emote)
elseif GetResourceState('rpemotes') == 'started' then
exports.rpemotes:EmoteCommandStart(emote)
elseif GetResourceState('dpemotes') == 'started' then
ExecuteCommand('e '..emote)
end
end)
---On close event listener
---@param type string 'new' | 'existing'
RegisterNetEvent('cad-citations:onClose', function(type)
if GetResourceState('scully_emotemenu') == 'started' then
exports.scully_emotemenu:cancelEmote()
elseif GetResourceState('rpemotes') == 'started' then
exports.rpemotes:EmoteCancel(true)
elseif GetResourceState('dpemotes') == 'started' then
ExecuteCommand('e c')
end
end)
---Interactions (QbTarget, OxTarget, OxLibTextUI, etc)
CreateThread(function()
local function createBlip(data)
local blip = AddBlipForCoord(data.coords.x, data.coords.y, data.coords.z)
SetBlipAsShortRange(blip, true)
SetBlipSprite(blip, data.blip.sprite)
SetBlipColour(blip, data.blip.color)
SetBlipScale(blip, data.blip.scale)
SetBlipDisplay(blip, 6)
BeginTextCommandSetBlipName('STRING')
AddTextComponentSubstringPlayerName(data.blip.label)
EndTextCommandSetBlipName(blip)
end
for name, data in pairs(Config.CitationLocations) do
-- Creates blip for the location
if data.blip and data.blip?.enabled then createBlip(data) end
if (Config.Target == 'auto' and GetResourceState('ox_target') == 'started') or Config.Target == 'ox_target' then
-- Ox Target
exports.ox_target:addSphereZone({
name = name,
coords = data.coords,
radius = data.radius or 1.0,
debug = Config.Debug,
drawSprite = true,
options = {
{
label = data.label or 'Public Citations',
icon = data.icon or 'fa-solid fa-file-invoice',
distance = data.distance or data.radius,
onSelect = function()
exports['cad-citations']:showDashboard()
end
}
}
})
elseif (Config.Target == 'auto' and GetResourceState('qb-target') == 'started') or Config.Target == 'qb-target' then
-- QB Target
exports['qb-target']:AddCircleZone(name, data.coords, data.radius, {
name = name,
debugPoly = Config.Debug
}, {
options = {
{
num = 1,
label = data.label or 'Public Citations',
icon = data.icon or 'fa-solid fa-file-invoice',
action = function()
exports['cad-citations']:showDashboard()
end
}
},
distance = data.distance or data.radius,
})
else
-- Ox Lib TextUI
local point = lib.points.new({
coords = data.coords,
distance = data.radius,
})
function point:onEnter()
lib.showTextUI(('[E] %s'):format(data.label), {
-- position = "top-center",
icon = data.icon,
})
end
function point:onExit()
lib.hideTextUI()
end
function point:nearby()
DrawMarker(20, self.coords.x, self.coords.y, self.coords.z + 0.5, 0.0, 0.0, 0.0, 0.0, 180.0, 0.0, 0.3, 0.3, 0.2, 100, 100, 200, 100, true, true, 2, false, nil, nil, false)
if self.currentDistance < (data.distance or data.radius) and IsControlJustReleased(0, 38) then
exports['cad-citations']:showDashboard()
end
end
end
end
end)
RegisterCommand('reset_citation', function()
exports['cad-citations']:resetUI()
end, false)bridge/utils/server.lua
lua
---Get Automated Data For: Player, Vehicle, Officer
---@param source string | integer
---@return table
function GetAutomatedInfo(source)
local data = {}
local ped = GetPlayerPed(source)
local coords = GetEntityCoords(ped)
local playerId = lib.getClosestPlayer(coords, 5.0, false)
local vehicleId = lib.getClosestVehicle(coords, 5.0, false)
if vehicleId and Config.AutoFetchData.vehicle then
data.vehicleModel, data.plateNumber = GetVehicleName(vehicleId), GetVehicleNumberPlateText(vehicleId)
end
if playerId and (playerId ~= source) and Config.AutoFetchData.player then
local player = GetPlayerInfo(playerId)
data.citizenId = player?.citizenId
data.fullName = player?.fullName
end
if source and Config.AutoFetchData.officer then
local player = GetPlayerInfo(source)
data.officerCitizenId = player?.citizenId
data.officerName = player?.fullName
data.officerCallsign = player?.callsign
end
return data
end
---Send Phone Mail
---@param data any
function SendMail(data)
if GetResourceState('lb-phone') == 'started' then
local result = MySQL.query.await("SELECT lia.username FROM phone_phones pp LEFT JOIN phone_logged_in_accounts lia ON lia.phone_number = pp.phone_number AND lia.app = 'Mail' where pp.owner_id = ?", { data.citizenId })
if not result or not result[1] then return false end
exports["lb-phone"]:SendMail({
to = result and result[1].username,
sender = 'Traffic Department',
subject = data.subject,
message = data.message
})
elseif GetResourceState('yseries') == 'started' then
local phoneNumber = exports.yseries:GetPhoneNumberByIdentifier(data.citizenId)
exports.yseries:SendMail({
title = data.subject,
sender = 'trafficdept@yflip.com',
senderDisplayName = 'Traffic Department',
content = data.message
}, 'phoneNumber', phoneNumber)
elseif GetResourceState('snappy-phone') == 'started' then
exports['snappy-phone']:sendCustomMail({
sender = 'Traffic Department',
subject = data.subject,
body = data.message,
recipients = {
{ identifier = data.citizenId }
}
})
else
assert(false, "No phone resource found.")
return false
end
end
---Create Warrant/BOLO
---@param data ICitation
function CreateWarrantBOLO(data)
end
---Add Fine Money to Account
---@param player table
---@param data ICitation
function AddMoneyToAccount(player, data)
if not Config.CitationAccountEnabled then return end
if not data?.officerJob or not data?.fineAmount then return end
local accountName, amount = data?.officerJob, data?.fineAmount
local commission = Config.CitationCommisions.commisions and Config.CitationCommisions.commisions[data?.officerJob]
if commission and commission > 0 then amount = amount - (amount * (commission/100)) end
local reason = 'Citation payment by ' .. player?.fullName .. ' for ' .. data?.plateNumber
if GetResourceState('snipe-banking') == 'started' then
exports['snipe-banking']:AddMoneyToAccount(accountName, amount)
elseif GetResourceState('Renewed-Banking') == 'started' then
return exports['Renewed-Banking']:addAccountMoney(accountName, amount)
elseif GetResourceState('qb-banking') == 'started' then
local status, business = pcall(function() return exports['qb-banking']:business(accountName, Config.CitationAccount.accountId) end)
if status and business then
return business.addBalance(amount, reason)
else
return exports['qb-banking']:AddMoney(accountName, amount, reason) or false
end
return business or false
elseif GetResourceState('okokBanking') == 'started' then
return exports['okokBanking']:AddMoney(accountName, amount)
elseif GetResourceState('tgg-banking') == 'started' then
return exports['tgg-banking']:AddSocietyMoney(accountName, amount)
elseif GetResourceState('fd_banking') == 'started' then
return exports['tgg-banking']:AddMoney(accountName, amount, reason)
elseif GetResourceState('kartik-banking') == 'started' then
return exports['kartik-banking']:AddAccountMoney(accountName, amount, reason)
else
assert(false, "No banking resource found.")
return false
end
end
---Provide Commision to officer
---@param player table
---@param data ICitation
function AddCommision(player, data)
if not Config.CitationCommisions or not Config.CitationCommisions?.enabled then return end
local commission = Config.CitationCommisions.commisions[data?.officerJob]
local fineAmount = data?.fineAmount and tonumber(data?.fineAmount) or 0
if commission and fineAmount > 0 then
local officer = GetPlayerInfo(data?.officerCitizenId)
if not officer then return end
local commissionAmount = lib.math.round(fineAmount*(commission/100), 2)
local reason = 'Citation commission from ' .. player?.fullName .. ' for ' .. data?.plateNumber
officer.addMoney(Config.CitationPaymentMethod, commissionAmount, reason)
end
end
---Logger wrapper
---@param source number | nil
---@param name string
---@param message string
---@param data table<any>
function Logger(source, name, message, data)
local webhook = ""
local title = ('[%d] - %s - %s'):format(source, GetPlayerName(source), name)
local headers = { ['Content-Type'] = 'application/json' }
local _data = json.encode({
username = "Cad Citations",
embeds = {
{
title = title,
color = 10650848,
author = {
name = "cadburry",
},
description = "\n** Message :** "..message.." \n **Data : ** "..json.encode(data).." ",
footer = {
text = os.date('%c'),
},
}
}
})
PerformHttpRequest(webhook, function() end, 'POST', _data, headers)
lib.logger(source, name, message, data)
end
---On Citation Event Triggered (Listener)
---@param event string "createCitation" | "updateCitation" | "payCitation" | "deleteCitation" | "warrantBolo"
---@param eventData IEventData
function OnEventAction(event, eventData)
if event == 'createCitation' then
if not eventData?.data?.citizenId or not eventData?.data?.plateNumber then return end
local player = GetPlayerInfoFromPlate(eventData?.data?.plateNumber)
if not player then return end
SendMail({
citizenId = player.citizenId,
phoneNumber = player.phoneNumber,
subject = 'New Citation',
message = 'You have been issued citation on '.. eventData.data.vehicleModel ..' ('.. eventData.data.plateNumber .. ') for $'.. eventData.data.fineAmount
})
end
if event == 'warrantBolo' then
if not eventData?.citizenId or not eventData?.data then return end
CreateWarrantBOLO(eventData?.data)
end
if event == 'payCitation' then
if not eventData?.citizenId or not eventData?.data then return end
local player = GetPlayerInfo(eventData?.citizenId)
if not player then return end
AddMoneyToAccount(player, eventData?.data)
AddCommision(player, eventData?.data)
end
end
table<any>
function AddItem(source, item, amount, metadata)
if GetResourceState('ox_inventory') == 'started' then
return exports.ox_inventory:AddItem(source, item, amount, metadata)
elseif GetResourceState('qb-inventory') == 'started' then
return exports['qb-inventory']:AddItem(source, item, amount, nil, metadata)
elseif GetResourceState('ps-inventory') == 'started' then
return exports['ps-inventory']:AddItem(source, item, amount, nil, metadata)
elseif GetResourceState('lj-inventory') == 'started' then
return exports['lj-inventory']:AddItem(source, item, amount, nil, metadata)
else
assert(false, "No inventory resource found.")
return false
end
end
---Remove item from inventory
---@param source number | nil
---@param item string
---@param amount number
---@param metadata table<any>
function RemoveItem(source, item, amount, metadata)
if GetResourceState('ox_inventory') == 'started' then
return exports.ox_inventory:AddItem(source, item, amount, metadata)
elseif GetResourceState('qb-inventory') == 'started' then
return exports['qb-inventory']:AddItem(source, item, amount, nil, metadata)
elseif GetResourceState('ps-inventory') == 'started' then
return exports['ps-inventory']:AddItem(source, item, amount, nil, metadata)
elseif GetResourceState('lj-inventory') == 'started' then
return exports['lj-inventory']:AddItem(source, item, amount, nil, metadata)
else
assert(false, "No inventory resource found.")
return false
end
end
---Get Automated Data For: Player, Vehicle, Officer
---@param source string | integer
---@return table
function GetAutomatedInfo(source)
local data = {}
local ped = GetPlayerPed(source)
local coords = GetEntityCoords(ped)
local playerId = lib.getClosestPlayer(coords, 5.0, false)
local vehicleId = lib.getClosestVehicle(coords, 5.0, false)
if vehicleId and Config.AutoFetchData.vehicle then
data.vehicleModel, data.plateNumber = GetVehicleName(vehicleId), GetVehicleNumberPlateText(vehicleId)
end
if playerId and (playerId ~= source) and Config.AutoFetchData.player then
local player = GetPlayerInfo(playerId)
data.citizenId = player?.citizenId
data.fullName = player?.fullName
end
if source and Config.AutoFetchData.officer then
local player = GetPlayerInfo(source)
data.officerCitizenId = player?.citizenId
data.officerName = player?.fullName
data.officerCallsign = player?.callsign
end
return data
end
---Send Phone Mail
---@param data any
function SendMail(data)
if GetResourceState('lb-phone') == 'started' then
local email = exports["lb-phone"]:GetEmailAddress(data.phoneNumber)
exports["lb-phone"]:SendMail({
to = email,
sender = 'Traffic Department',
subject = data.subject,
message = data.message
})
elseif GetResourceState('yseries') == 'started' then
local phoneNumber = exports.yseries:GetPhoneNumberByIdentifier(data.citizenId)
exports.yseries:SendMail({
title = data.subject,
sender = 'trafficdept@yflip.com',
senderDisplayName = 'Traffic Department',
content = data.message
}, 'phoneNumber', phoneNumber)
elseif GetResourceState('snappy-phone') == 'started' then
exports['snappy-phone']:sendCustomMail({
sender = 'Traffic Department',
subject = data.subject,
body = data.message,
recipients = {
{ identifier = data.citizenId }
}
})
else
assert(false, "No phone resource found.")
return false
end
end
---Create Warrant/BOLO
---@param data ICitation
function CreateWarrantBOLO(data)
end
---Add Fine Money to Account
---@param player table
---@param data ICitation
function AddMoneyToAccount(player, data)
if not Config.CitationAccountEnabled then return end
if not data?.officerJob or not data?.fineAmount then return end
local accountName, amount = data?.officerJob, data?.fineAmount
local reason = 'Citation payment by ' .. player?.fullName .. ' for ' .. data?.plateNumber
if GetResourceState('snipe-banking') == 'started' then
exports['snipe-banking']:AddMoneyToAccount(accountName, amount)
elseif GetResourceState('Renewed-Banking') == 'started' then
return exports['Renewed-Banking']:addAccountMoney(accountName, amount)
elseif GetResourceState('qb-banking') == 'started' then
local status, business = pcall(function() return exports['qb-banking']:business(accountName, Config.CitationAccount.accountId) end)
if status and business then
return business.addBalance(amount, reason)
else
return exports['qb-banking']:AddMoney(accountName, amount, reason) or false
end
return business or false
elseif GetResourceState('okokBanking') == 'started' then
return exports['okokBanking']:AddMoney(accountName, amount)
elseif GetResourceState('tgg-banking') == 'started' then
return exports['tgg-banking']:AddSocietyMoney(accountName, amount)
elseif GetResourceState('fd_banking') == 'started' then
return exports['tgg-banking']:AddMoney(accountName, amount, reason)
elseif GetResourceState('kartik-banking') == 'started' then
return exports['kartik-banking']:AddAccountMoney(accountName, amount, reason)
else
assert(false, "No banking resource found.")
return false
end
end
---Provide Commision to officer
---@param player table
---@param data ICitation
function AddCommision(player, data)
if not Config.CitationCommisions or not Config.CitationCommisions?.enabled then return end
local commission = Config.CitationCommisions.commisions[data?.officerJob]
local fineAmount = data?.fineAmount and tonumber(data?.fineAmount) or 0
if commission and fineAmount > 0 then
local officer = GetPlayerInfo(data?.officerCitizenId)
if not officer then return end
local commissionAmount = lib.math.round(fineAmount*commission, 2)
local reason = 'Citation commission from ' .. player?.fullName .. ' for ' .. data?.plateNumber
officer.addMoney(Config.CitationPaymentMethod, commissionAmount, reason)
end
end
---Logger wrapper
---@param source number | nil
---@param name string
---@param message string
---@param data table<any>
function Logger(source, name, message, data)
local webhook = ""
local title = ('[%d] - %s - %s'):format(source, GetPlayerName(source), name)
local headers = { ['Content-Type'] = 'application/json' }
local _data = json.encode({
username = "Cad Citations",
embeds = {
{
title = title,
color = 10650848,
author = {
name = "cadburry",
},
description = "\n** Message :** "..message.." \n **Data : ** "..json.encode(data).." ",
footer = {
text = os.date('%c'),
},
}
}
})
PerformHttpRequest(webhook, function() end, 'POST', _data, headers)
lib.logger(source, name, message, data)
end
---This function is used to handle item usage, specifically for ox_inventory.
---@param event string
---@param item table<any>
---@param inv table<any>
---@param slot number
---@param data table<any>
exports('useItem', function(event, item, inv, slot, data)
if event == 'usingItem' then
if item.name == Config.Items.show_receipt then
local _item = GetItemBySlot(inv.id, slot)
assert(_item, "Item not found in inventory: " .. tostring(slot))
exports['cad-citations']:showCitation(inv.id, _item.metadata)
return false
end
if item.name == Config.Items.create_citation then
local citation = GetAutomatedInfo(inv.id)
exports['cad-citations']:createCitation(inv.id, citation)
return false
end
return
end
end)
---On Citation Event Triggered (Listener)
---@param event string "createCitation" | "updateCitation" | "payCitation" | "deleteCitation" | "warrantBolo"
---@param eventData IEventData
function OnEventAction(event, eventData)
if event == 'createCitation' then
if not eventData?.data?.citizenId or not eventData?.data?.plateNumber then return end
local player = GetPlayerInfoFromPlate(eventData?.data?.plateNumber)
if not player then return end
SendMail({
citizenId = player.citizenId,
phoneNumber = player.phoneNumber,
subject = 'New Citation',
message = 'You have been issued citation on '.. eventData.data.vehicleModel ..' ('.. eventData.data.plateNumber .. ') for $'.. eventData.data.fineAmount
})
end
if event == 'warrantBolo' then
if not eventData?.citizenId or not eventData?.data then return end
CreateWarrantBOLO(eventData?.data)
end
if event == 'payCitation' then
if not eventData?.citizenId or not eventData?.data then return end
local player = GetPlayerInfo(eventData?.citizenId)
if not player then return end
AddMoneyToAccount(player, eventData?.data)
AddCommision(player, eventData?.data)
end
end