Files
DerEchteAlec a9143b0fba ADD - add in-game phone administration and configuration (#25)
* 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

---------

Co-authored-by: Leon.Schmidt <leonmauricewill15@gmail.com>
2026-08-22 03:26:52 +02:00

455 lines
12 KiB
Lua

local APP_ID_MAX_LENGTH = 64
local DESCRIPTION_MAX_LENGTH = 320
local DEVELOPER_MAX_LENGTH = 96
local NAME_MAX_LENGTH = 64
local PATH_MAX_LENGTH = 512
local VERSION_MAX_LENGTH = 32
local ALLOWED_CATEGORIES = {
games = true,
productivity = true,
shopping = true,
social = true,
utilities = true,
}
local ALLOWED_ORIENTATIONS = {
any = true,
landscape = true,
portrait = true,
}
local ALLOWED_PERMISSIONS = {
["app.close"] = true,
["app.open"] = true,
["camera.capture"] = true,
["contacts.pick"] = true,
["device.storage"] = true,
["locale.read"] = true,
["location.read"] = true,
["media.pick"] = true,
["notifications"] = true,
["notifications.critical"] = true,
["nui.fetch"] = true,
["theme.read"] = true,
}
local RESERVED_APP_IDS = {
admin = true,
["app-store"] = true,
banking = true,
crypto = true,
billing = true,
calculator = true,
calendar = true,
camera = true,
citymarkt = true,
clock = true,
companies = true,
crewlink = true,
darkchat = true,
feather = true,
flare = true,
fliptok = true,
garage = true,
house = true,
["local-pages"] = true,
mail = true,
map = true,
memory = true,
messages = true,
minesweeper = true,
music = true,
["neon-drop"] = true,
notes = true,
memos = true,
["number-merge"] = true,
phone = true,
photos = true,
picstagram = true,
radio = true,
settings = true,
["sky-flappy"] = true,
skyride = true,
snake = true,
["tower-stack"] = true,
weather = true,
}
local bundled_manifests = {}
local bundled_manifests_by_id = {}
SkyPhoneApps = SkyPhoneApps or {}
SkyPhoneApps.ProtocolVersion = 1
local function trim(value)
return value:match("^%s*(.-)%s*$")
end
local function validate_app_id(app_id)
if type(app_id) ~= "string" then
return false, "invalid_app_id"
end
if #app_id == 0 or #app_id > APP_ID_MAX_LENGTH then
return false, "invalid_app_id"
end
if not app_id:match("^[a-z0-9][a-z0-9._-]+$") then
return false, "invalid_app_id"
end
return true
end
local function validate_text(value, error_code, maximum_length, required)
if value == nil and not required then
return nil
end
if type(value) ~= "string" then
return nil, error_code
end
local normalized = trim(value)
if (required and #normalized == 0) or #normalized > maximum_length then
return nil, error_code
end
return normalized
end
local function validate_localized_text(value, error_code, maximum_length, required)
if type(value) == "string" then
return validate_text(value, error_code, maximum_length, required)
end
if type(value) ~= "table" then
return nil, error_code
end
local normalized = {}
local count = 0
for locale, text in pairs(value) do
if type(locale) ~= "string" or not locale:match("^[a-zA-Z][a-zA-Z0-9_-]*$") then
return nil, error_code
end
local normalized_text = validate_text(text, error_code, maximum_length, true)
if not normalized_text then
return nil, error_code
end
count = count + 1
if count > 16 then
return nil, error_code
end
normalized[locale:lower():gsub("_", "-")] = normalized_text
end
if count == 0 then
return nil, error_code
end
return normalized
end
local function validate_local_path(path, required)
if path == nil and not required then
return nil
end
if type(path) ~= "string" or #path == 0 or #path > PATH_MAX_LENGTH then
return nil, "invalid_local_path"
end
if path:sub(1, 1) == "/" or path:find("\\", 1, true) or path:find(":", 1, true) then
return nil, "invalid_local_path"
end
if not path:match("^web/[%w%._%-%/]+$") then
return nil, "invalid_local_path"
end
for segment in path:gmatch("[^/]+") do
if segment == "." or segment == ".." then
return nil, "invalid_local_path"
end
end
return path
end
local function validate_string_array(value, error_code, maximum_items, validator)
if value == nil then
return {}
end
if type(value) ~= "table" then
return nil, error_code
end
local normalized = {}
local count = 0
for key in pairs(value) do
if type(key) ~= "number" or key < 1 or key % 1 ~= 0 then
return nil, error_code
end
count = count + 1
end
if count ~= #value or count > maximum_items then
return nil, error_code
end
for index = 1, count do
local item = value[index]
if type(item) ~= "string" then
return nil, error_code
end
local normalized_item, validation_error = validator(item)
if not normalized_item then
return nil, validation_error or error_code
end
normalized[index] = normalized_item
end
return normalized
end
local function validate_permissions(value)
local permissions, error_code = validate_string_array(
value,
"invalid_permissions",
32,
function(permission)
if not ALLOWED_PERMISSIONS[permission] then
return nil, "unknown_permission"
end
return permission
end
)
if not permissions then
return nil, error_code
end
local seen = {}
for index = 1, #permissions do
local permission = permissions[index]
if seen[permission] then
return nil, "duplicate_permission"
end
seen[permission] = true
end
return permissions
end
local function validate_grid_order(value)
if value == nil then
return nil
end
if type(value) ~= "number" or value ~= math.floor(value) or value < 0 or value > 9999 then
return nil, "invalid_grid_order"
end
return value
end
local function validate_icon_background(value)
if value == nil then
return nil
end
if type(value) ~= "string" or #value == 0 or #value > 64 then
return nil, "invalid_icon_background"
end
if value:find("[\r\n;{}]") then
return nil, "invalid_icon_background"
end
return value
end
local function normalize_manifest(folder, definition)
if type(folder) ~= "string" or type(definition) ~= "table" then
return nil, "invalid_manifest"
end
if definition.schemaVersion ~= SkyPhoneApps.ProtocolVersion then
return nil, "unsupported_schema_version"
end
local app_id = definition.id
local valid_id, id_error = validate_app_id(app_id)
if not valid_id then
return nil, id_error
end
if RESERVED_APP_IDS[app_id] then
return nil, "reserved_app_id"
end
if folder ~= app_id then
return nil, "folder_id_mismatch"
end
if definition.defaultInstalled ~= nil and type(definition.defaultInstalled) ~= "boolean" then
return nil, "invalid_default_installed"
end
if definition.removable ~= nil and type(definition.removable) ~= "boolean" then
return nil, "invalid_removable"
end
local version, version_error = validate_text(
definition.version or "1.0.0",
"invalid_version",
VERSION_MAX_LENGTH,
true
)
if not version then
return nil, version_error
end
if not version:match("^[%w%._+-]+$") then
return nil, "invalid_version"
end
local name, name_error = validate_localized_text(
definition.name,
"invalid_name",
NAME_MAX_LENGTH,
true
)
if not name then
return nil, name_error
end
local description, description_error = validate_localized_text(
definition.description,
"invalid_description",
DESCRIPTION_MAX_LENGTH,
true
)
if not description then
return nil, description_error
end
local developer, developer_error = validate_text(
definition.developer,
"invalid_developer",
DEVELOPER_MAX_LENGTH,
false
)
if definition.developer ~= nil and not developer then
return nil, developer_error
end
local category = definition.category or "utilities"
if not ALLOWED_CATEGORIES[category] then
return nil, "invalid_category"
end
local entry, entry_error = validate_local_path(definition.entry, true)
if not entry then
return nil, entry_error
end
local icon, icon_error = validate_local_path(definition.icon, false)
if definition.icon ~= nil and not icon then
return nil, icon_error
end
local screenshots, screenshots_error = validate_string_array(
definition.screenshots,
"invalid_screenshots",
8,
function(path)
return validate_local_path(path, true)
end
)
if not screenshots then
return nil, screenshots_error
end
local permissions, permissions_error = validate_permissions(definition.permissions)
if not permissions then
return nil, permissions_error
end
local orientation = definition.orientation or "portrait"
if not ALLOWED_ORIENTATIONS[orientation] then
return nil, "invalid_orientation"
end
local grid_order, grid_order_error = validate_grid_order(definition.gridOrder)
if definition.gridOrder ~= nil and not grid_order then
return nil, grid_order_error
end
local icon_background, background_error = validate_icon_background(definition.iconBackground)
if definition.iconBackground ~= nil and not icon_background then
return nil, background_error
end
local bridge_mode = definition.bridgeMode or "sky"
if bridge_mode ~= "sky" and bridge_mode ~= "legacy" then
return nil, "invalid_bridge_mode"
end
return {
bridgeMode = bridge_mode,
category = category,
defaultInstalled = definition.defaultInstalled == true,
description = description,
developer = developer,
entry = entry,
folder = folder,
gridOrder = grid_order,
icon = icon,
iconBackground = icon_background,
id = app_id,
name = name,
orientation = orientation,
permissions = permissions,
removable = definition.removable ~= false,
schemaVersion = SkyPhoneApps.ProtocolVersion,
screenshots = screenshots,
version = version,
}
end
---@param folder string App folder relative to custom_apps.
---@param definition table Declarative bundled app manifest.
function SkyPhoneApps.RegisterManifest(folder, definition)
local manifest, error_code = normalize_manifest(folder, definition)
if not manifest then
error(("[sky_phone] Invalid custom app manifest '%s': %s"):format(tostring(folder), error_code), 2)
end
if bundled_manifests_by_id[manifest.id] then
error(("[sky_phone] Duplicate bundled custom app id '%s'."):format(manifest.id), 2)
end
bundled_manifests[#bundled_manifests + 1] = manifest
bundled_manifests_by_id[manifest.id] = manifest
end
function SkyPhoneApps.GetBundledManifests()
return bundled_manifests
end
function SkyPhoneApps.GetBundledManifest(app_id)
return bundled_manifests_by_id[app_id]
end
SkyPhoneApps.AllowedCategories = ALLOWED_CATEGORIES
SkyPhoneApps.AllowedOrientations = ALLOWED_ORIENTATIONS
SkyPhoneApps.AllowedPermissions = ALLOWED_PERMISSIONS
SkyPhoneApps.ReservedAppIds = RESERVED_APP_IDS
SkyPhoneApps.ValidateAppId = validate_app_id
SkyPhoneApps.ValidateLocalizedText = validate_localized_text
SkyPhoneApps.ValidatePermissions = validate_permissions