mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-28 23:01:37 +00:00
edd9677f43
* ADD - build in-game admin command center * ADD - open admin panel by command * ENH - rebuild admin panel as standalone editor * ENH - compact admin workspace navigation * ENH - expand admin device controls * ENH - refine admin panel navigation style * ADD - add SQL phone configurator * FIX - expose complete phone configuration * FIX - make company jobs freely configurable * FIX - align configurator tables to left * ENH - organize configurator collection controls * ENH - organize nested configurator sections * ENH - add configurator field descriptions * FIX - hide fixed configurator add controls * ENH - refine configurator editing experience * ENH - refine admin configurator controls * FIX - apply configurator settings instantly * FIX - close live activity command registration
539 lines
16 KiB
Lua
539 lines
16 KiB
Lua
local policies_by_id = {}
|
|
local policy_ids_by_adapter = {}
|
|
local policy_ids_by_owner = {}
|
|
local current_resource = GetCurrentResourceName()
|
|
|
|
local function validate_owner_resource(owner_resource, allow_stopping)
|
|
if type(owner_resource) ~= "string"
|
|
or #owner_resource == 0
|
|
or #owner_resource > 64
|
|
or not owner_resource:match("^[%w][%w._-]*$")
|
|
then
|
|
return false, "invalid_owner_resource"
|
|
end
|
|
|
|
local state = GetResourceState(owner_resource)
|
|
if state == "started" or state == "starting" or (allow_stopping and state == "stopping") then
|
|
return true
|
|
end
|
|
|
|
return false, "owner_resource_not_running"
|
|
end
|
|
|
|
local function add_owner_index(index, resource_name, app_id)
|
|
local app_ids = index[resource_name]
|
|
if not app_ids then
|
|
app_ids = {}
|
|
index[resource_name] = app_ids
|
|
end
|
|
app_ids[app_id] = true
|
|
end
|
|
|
|
local function remove_owner_index(index, resource_name, app_id)
|
|
local app_ids = index[resource_name]
|
|
if not app_ids then
|
|
return
|
|
end
|
|
|
|
app_ids[app_id] = nil
|
|
if not next(app_ids) then
|
|
index[resource_name] = nil
|
|
end
|
|
end
|
|
|
|
local function build_permission_set(permissions)
|
|
local permission_set = {}
|
|
for index = 1, #permissions do
|
|
permission_set[permissions[index]] = true
|
|
end
|
|
return permission_set
|
|
end
|
|
|
|
local function register_policy(policy)
|
|
local app_id = policy.id
|
|
local existing = policies_by_id[app_id]
|
|
if existing then
|
|
Bridge.Debug(
|
|
"warn",
|
|
"[sky_phone] Rejected duplicate custom app policy '%s' from '%s'; it is already owned by '%s'.",
|
|
app_id,
|
|
policy.ownerResource,
|
|
existing.ownerResource
|
|
)
|
|
return false, "duplicate_app_id"
|
|
end
|
|
|
|
policies_by_id[app_id] = policy
|
|
add_owner_index(policy_ids_by_owner, policy.ownerResource, app_id)
|
|
if policy.adapterResource then
|
|
add_owner_index(policy_ids_by_adapter, policy.adapterResource, app_id)
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function remove_policy(app_id)
|
|
local policy = policies_by_id[app_id]
|
|
if not policy then
|
|
return false, "app_not_found"
|
|
end
|
|
|
|
policies_by_id[app_id] = nil
|
|
remove_owner_index(policy_ids_by_owner, policy.ownerResource, app_id)
|
|
if policy.adapterResource then
|
|
remove_owner_index(policy_ids_by_adapter, policy.adapterResource, app_id)
|
|
end
|
|
return true
|
|
end
|
|
|
|
local function get_direct_owner()
|
|
local owner_resource = GetInvokingResource()
|
|
if not owner_resource then
|
|
return nil, "missing_invoking_resource"
|
|
end
|
|
|
|
local valid_owner, owner_error = validate_owner_resource(owner_resource, false)
|
|
if not valid_owner then
|
|
return nil, owner_error
|
|
end
|
|
|
|
return owner_resource
|
|
end
|
|
|
|
local function get_trusted_adapter()
|
|
local adapter_resource = GetInvokingResource()
|
|
if not adapter_resource or not Config.CustomApps.TrustedAdapters[adapter_resource] then
|
|
return nil, "untrusted_adapter"
|
|
end
|
|
|
|
local valid_adapter, adapter_error = validate_owner_resource(adapter_resource, false)
|
|
if not valid_adapter then
|
|
return nil, adapter_error
|
|
end
|
|
|
|
return adapter_resource
|
|
end
|
|
|
|
local function normalize_policy(owner_resource, adapter_resource, definition)
|
|
if type(definition) ~= "table" then
|
|
return nil, "invalid_definition"
|
|
end
|
|
if definition.schemaVersion ~= nil and definition.schemaVersion ~= SkyPhoneApps.ProtocolVersion then
|
|
return nil, "unsupported_schema_version"
|
|
end
|
|
|
|
local valid_id, id_error = SkyPhoneApps.ValidateAppId(definition.id)
|
|
if not valid_id then
|
|
return nil, id_error
|
|
end
|
|
if SkyPhoneApps.ReservedAppIds[definition.id] then
|
|
return nil, "reserved_app_id"
|
|
end
|
|
|
|
local permissions, permissions_error = SkyPhoneApps.ValidatePermissions(definition.permissions)
|
|
if not permissions then
|
|
return nil, permissions_error
|
|
end
|
|
|
|
return {
|
|
adapterResource = adapter_resource,
|
|
bundled = false,
|
|
id = definition.id,
|
|
ownerResource = owner_resource,
|
|
permissions = build_permission_set(permissions),
|
|
}
|
|
end
|
|
|
|
local function add_custom_app_policy(definition)
|
|
if not Config.CustomApps.Enabled or not Config.CustomApps.ExternalApps then
|
|
return false, "external_apps_disabled"
|
|
end
|
|
|
|
local owner_resource, owner_error = get_direct_owner()
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
|
|
local policy, policy_error = normalize_policy(owner_resource, nil, definition)
|
|
if not policy then
|
|
return false, policy_error
|
|
end
|
|
return register_policy(policy)
|
|
end
|
|
|
|
local function add_custom_app_policy_from_adapter(owner_resource, definition)
|
|
if not Config.CustomApps.Enabled or not Config.CustomApps.ExternalApps then
|
|
return false, "external_apps_disabled"
|
|
end
|
|
|
|
local adapter_resource, adapter_error = get_trusted_adapter()
|
|
if not adapter_resource then
|
|
return false, adapter_error
|
|
end
|
|
local valid_owner, owner_error = validate_owner_resource(owner_resource, false)
|
|
if not valid_owner then
|
|
return false, owner_error
|
|
end
|
|
|
|
local policy, policy_error = normalize_policy(owner_resource, adapter_resource, definition)
|
|
if not policy then
|
|
return false, policy_error
|
|
end
|
|
return register_policy(policy)
|
|
end
|
|
|
|
local function verify_owned_policy(owner_resource, app_id, adapter_resource)
|
|
local valid_id, id_error = SkyPhoneApps.ValidateAppId(app_id)
|
|
if not valid_id then
|
|
return nil, id_error
|
|
end
|
|
|
|
local policy = policies_by_id[app_id]
|
|
if not policy then
|
|
return nil, "app_not_found"
|
|
end
|
|
if policy.ownerResource ~= owner_resource then
|
|
return nil, "app_owner_mismatch"
|
|
end
|
|
if adapter_resource and policy.adapterResource ~= adapter_resource then
|
|
return nil, "app_adapter_mismatch"
|
|
end
|
|
|
|
return policy
|
|
end
|
|
|
|
local function replace_policy(policy, adapter_resource)
|
|
local existing, policy_error = verify_owned_policy(
|
|
policy.ownerResource,
|
|
policy.id,
|
|
adapter_resource
|
|
)
|
|
if not existing then
|
|
return false, policy_error
|
|
end
|
|
if existing.bundled then
|
|
return false, "bundled_app"
|
|
end
|
|
|
|
policies_by_id[policy.id] = policy
|
|
return true
|
|
end
|
|
|
|
local function update_custom_app_policy(definition)
|
|
if not Config.CustomApps.Enabled or not Config.CustomApps.ExternalApps then
|
|
return false, "external_apps_disabled"
|
|
end
|
|
|
|
local owner_resource, owner_error = get_direct_owner()
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
|
|
local policy, policy_error = normalize_policy(owner_resource, nil, definition)
|
|
if not policy then
|
|
return false, policy_error
|
|
end
|
|
return replace_policy(policy, nil)
|
|
end
|
|
|
|
local function update_custom_app_policy_from_adapter(owner_resource, definition)
|
|
if not Config.CustomApps.Enabled or not Config.CustomApps.ExternalApps then
|
|
return false, "external_apps_disabled"
|
|
end
|
|
|
|
local adapter_resource, adapter_error = get_trusted_adapter()
|
|
if not adapter_resource then
|
|
return false, adapter_error
|
|
end
|
|
local valid_owner, owner_error = validate_owner_resource(owner_resource, false)
|
|
if not valid_owner then
|
|
return false, owner_error
|
|
end
|
|
|
|
local policy, policy_error = normalize_policy(owner_resource, adapter_resource, definition)
|
|
if not policy then
|
|
return false, policy_error
|
|
end
|
|
return replace_policy(policy, adapter_resource)
|
|
end
|
|
|
|
local function remove_custom_app_policy(app_id)
|
|
local owner_resource, owner_error = get_direct_owner()
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
|
|
local policy, policy_error = verify_owned_policy(owner_resource, app_id)
|
|
if not policy then
|
|
return false, policy_error
|
|
end
|
|
if policy.bundled then
|
|
return false, "bundled_app"
|
|
end
|
|
|
|
return remove_policy(app_id)
|
|
end
|
|
|
|
local function remove_custom_app_policy_from_adapter(owner_resource, app_id)
|
|
local adapter_resource, adapter_error = get_trusted_adapter()
|
|
if not adapter_resource then
|
|
return false, adapter_error
|
|
end
|
|
|
|
local policy, policy_error = verify_owned_policy(owner_resource, app_id, adapter_resource)
|
|
if not policy then
|
|
return false, policy_error
|
|
end
|
|
return remove_policy(app_id)
|
|
end
|
|
|
|
local function has_custom_app_permission(app_id, permission)
|
|
local valid_id = SkyPhoneApps.ValidateAppId(app_id)
|
|
if not valid_id or type(permission) ~= "string" or not SkyPhoneApps.AllowedPermissions[permission] then
|
|
return false
|
|
end
|
|
|
|
local policy = policies_by_id[app_id]
|
|
return policy ~= nil and policy.permissions[permission] == true
|
|
end
|
|
|
|
local function get_custom_app_policy(app_id)
|
|
local valid_id = SkyPhoneApps.ValidateAppId(app_id)
|
|
if not valid_id then
|
|
return nil
|
|
end
|
|
|
|
local policy = policies_by_id[app_id]
|
|
if not policy then
|
|
return nil
|
|
end
|
|
|
|
local permissions = {}
|
|
for permission in pairs(policy.permissions) do
|
|
permissions[#permissions + 1] = permission
|
|
end
|
|
table.sort(permissions)
|
|
return {
|
|
bundled = policy.bundled,
|
|
id = policy.id,
|
|
ownerResource = policy.ownerResource,
|
|
permissions = permissions,
|
|
}
|
|
end
|
|
|
|
local function get_custom_app_capabilities()
|
|
return {
|
|
abiVersion = 1,
|
|
enabled = Config.CustomApps.Enabled == true,
|
|
exports = {
|
|
"AddCustomAppPolicy",
|
|
"GetCustomAppCapabilities",
|
|
"GetCustomAppPolicy",
|
|
"HasCustomAppPermission",
|
|
"RemoveCustomAppPolicy",
|
|
"SendAppMessage",
|
|
"SendCustomAppMessage",
|
|
"SendCustomAppNotification",
|
|
"UpdateCustomAppPolicy",
|
|
},
|
|
externalApps = Config.CustomApps.Enabled == true
|
|
and Config.CustomApps.ExternalApps == true,
|
|
messageDispatch = true,
|
|
notificationDispatch = true,
|
|
policyRegistration = true,
|
|
policyUpdate = true,
|
|
protocolVersion = SkyPhoneApps.ProtocolVersion,
|
|
}
|
|
end
|
|
|
|
local function normalize_player_source(player_source)
|
|
if type(player_source) ~= "number"
|
|
or player_source ~= player_source
|
|
or player_source <= 0
|
|
or player_source >= math.huge
|
|
or player_source ~= math.floor(player_source)
|
|
then
|
|
return nil
|
|
end
|
|
return player_source
|
|
end
|
|
|
|
local function normalize_message_payload(payload)
|
|
local encoded_success, encoded = pcall(json.encode, payload)
|
|
if not encoded_success or type(encoded) ~= "string" then
|
|
return nil, "invalid_payload"
|
|
end
|
|
if #encoded > Config.CustomApps.MaximumMessageBytes then
|
|
return nil, "payload_too_large"
|
|
end
|
|
|
|
local decoded_success, decoded = pcall(json.decode, encoded)
|
|
if not decoded_success then
|
|
return nil, "invalid_payload"
|
|
end
|
|
return decoded
|
|
end
|
|
|
|
local function send_custom_app_message(player_source, app_id, payload)
|
|
local owner_resource, owner_error = get_direct_owner()
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
|
|
local policy, policy_error = verify_owned_policy(owner_resource, app_id)
|
|
if not policy then
|
|
return false, policy_error
|
|
end
|
|
|
|
player_source = normalize_player_source(player_source)
|
|
if not player_source then
|
|
return false, "invalid_source"
|
|
end
|
|
if not Bridge.Framework.GetIdentifier(player_source) then
|
|
return false, "player_unavailable"
|
|
end
|
|
|
|
local normalized_payload, payload_error = normalize_message_payload(payload)
|
|
if payload_error then
|
|
return false, payload_error
|
|
end
|
|
|
|
TriggerClientEvent(
|
|
"sky_phone:custom-app:message",
|
|
player_source,
|
|
owner_resource,
|
|
app_id,
|
|
normalized_payload
|
|
)
|
|
return true
|
|
end
|
|
|
|
local function send_custom_app_notification(player_source, app_id, notification)
|
|
local owner_resource, owner_error = get_direct_owner()
|
|
if not owner_resource then
|
|
return false, owner_error
|
|
end
|
|
|
|
local policy, policy_error = verify_owned_policy(owner_resource, app_id)
|
|
if not policy then
|
|
return false, policy_error
|
|
end
|
|
if not policy.permissions.notifications then
|
|
return false, "permission_denied"
|
|
end
|
|
|
|
player_source = normalize_player_source(player_source)
|
|
if not player_source then
|
|
return false, "invalid_source"
|
|
end
|
|
if type(notification) ~= "table" then
|
|
return false, "invalid_notification"
|
|
end
|
|
if notification.appId ~= nil and notification.appId ~= app_id then
|
|
return false, "invalid_app_id"
|
|
end
|
|
if notification.app_id ~= nil and notification.app_id ~= app_id then
|
|
return false, "invalid_app_id"
|
|
end
|
|
|
|
local notifications = SkyPhoneNotifications
|
|
if type(notifications) ~= "table" or type(notifications.Send) ~= "function" then
|
|
return false, "api_not_ready"
|
|
end
|
|
|
|
local result, notification_error = notifications.Send(
|
|
{ kind = "source", value = player_source },
|
|
{
|
|
appId = app_id,
|
|
text = notification.text or notification.content,
|
|
title = notification.title,
|
|
}
|
|
)
|
|
if not result then
|
|
return false, notification_error
|
|
end
|
|
if result.delivered ~= 1 then
|
|
return false, "device_not_equipped"
|
|
end
|
|
return true, result
|
|
end
|
|
|
|
SkyPhoneApps.GetPolicy = get_custom_app_policy
|
|
SkyPhoneApps.HasPermission = has_custom_app_permission
|
|
SkyPhoneApps.ServerPublicApi = {
|
|
AddCustomAppPolicy = add_custom_app_policy,
|
|
AddCustomAppPolicyFromAdapter = add_custom_app_policy_from_adapter,
|
|
GetCustomAppCapabilities = get_custom_app_capabilities,
|
|
GetCustomAppPolicy = get_custom_app_policy,
|
|
HasCustomAppPermission = has_custom_app_permission,
|
|
RemoveCustomAppPolicy = remove_custom_app_policy,
|
|
RemoveCustomAppPolicyFromAdapter = remove_custom_app_policy_from_adapter,
|
|
SendAppMessage = send_custom_app_message,
|
|
SendCustomAppMessage = send_custom_app_message,
|
|
SendCustomAppNotification = send_custom_app_notification,
|
|
UpdateCustomAppPolicy = update_custom_app_policy,
|
|
UpdateCustomAppPolicyFromAdapter = update_custom_app_policy_from_adapter,
|
|
}
|
|
|
|
local function refresh_bundled_policies()
|
|
local removals = {}
|
|
for app_id, policy in pairs(policies_by_id) do
|
|
if policy.bundled then
|
|
removals[#removals + 1] = app_id
|
|
end
|
|
end
|
|
for _, app_id in ipairs(removals) do
|
|
remove_policy(app_id)
|
|
end
|
|
|
|
if Config.CustomApps.Enabled and Config.CustomApps.BundledApps then
|
|
local bundled = SkyPhoneApps.GetBundledManifests()
|
|
for index = 1, #bundled do
|
|
local manifest = bundled[index]
|
|
local registered, register_error = register_policy({
|
|
adapterResource = nil,
|
|
bundled = true,
|
|
id = manifest.id,
|
|
ownerResource = current_resource,
|
|
permissions = build_permission_set(manifest.permissions),
|
|
})
|
|
if not registered then
|
|
error(("[sky_phone] Could not register bundled custom app policy '%s': %s"):format(
|
|
manifest.id,
|
|
register_error
|
|
))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
refresh_bundled_policies()
|
|
|
|
AddEventHandler("sky_phone:configurator:serverUpdated", function()
|
|
refresh_bundled_policies()
|
|
end)
|
|
|
|
AddEventHandler("onResourceStop", function(resource_name)
|
|
if resource_name == current_resource then
|
|
return
|
|
end
|
|
|
|
local app_ids = {}
|
|
local owner_policies = policy_ids_by_owner[resource_name]
|
|
if owner_policies then
|
|
for app_id in pairs(owner_policies) do
|
|
app_ids[app_id] = true
|
|
end
|
|
end
|
|
|
|
local adapter_policies = policy_ids_by_adapter[resource_name]
|
|
if adapter_policies then
|
|
for app_id in pairs(adapter_policies) do
|
|
app_ids[app_id] = true
|
|
end
|
|
end
|
|
|
|
for app_id in pairs(app_ids) do
|
|
remove_policy(app_id)
|
|
end
|
|
end)
|