mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-31 10:18:57 +00:00
ADD - integrate phone housing providers
This commit is contained in:
@@ -14,7 +14,7 @@ Config.Bridge = {
|
||||
Config.Command = "phone"
|
||||
|
||||
Config.Phone = {
|
||||
Item = "sky_phone",
|
||||
Item = "phone",
|
||||
DevelopmentCommand = true,
|
||||
DeviceName = "iFruit Phone",
|
||||
}
|
||||
@@ -212,6 +212,29 @@ Config.Garage = {
|
||||
},
|
||||
}
|
||||
|
||||
Config.Housing = {
|
||||
System = "auto", -- auto, esx_property, qbx_properties
|
||||
AutoPriority = { "esx_property", "qbx_properties" },
|
||||
MaximumProperties = 50,
|
||||
OverviewRequestsPerMinute = 30,
|
||||
ActionsPerMinute = 12,
|
||||
Camera = {
|
||||
HeightAboveEntrance = 2.5,
|
||||
FieldOfView = 55.0,
|
||||
MinimumFieldOfView = 20.0,
|
||||
MaximumFieldOfView = 75.0,
|
||||
RotateSpeed = 0.65,
|
||||
ExitControl = 177, -- Backspace
|
||||
NightVisionControl = 38, -- E
|
||||
ZoomInControl = 241, -- Mouse wheel up
|
||||
ZoomOutControl = 242, -- Mouse wheel down
|
||||
LeftControl = 174,
|
||||
RightControl = 175,
|
||||
UpControl = 172,
|
||||
DownControl = 173,
|
||||
},
|
||||
}
|
||||
|
||||
Config.Marketplace = {
|
||||
PageSize = 20,
|
||||
MessagePageSize = 50,
|
||||
|
||||
@@ -484,6 +484,37 @@ Locales["en"] = {
|
||||
device_locked = "Unlock this phone to use SkyRide.",
|
||||
default = "SkyRide could not complete the request.",
|
||||
},
|
||||
},
|
||||
house = {
|
||||
name = "House", subtitle = "Homes and access", myHomes = "My Homes",
|
||||
properties = "Properties", owned = "Owned", shared = "Shared with you",
|
||||
locked = "Locked", unlocked = "Unlocked", owner = "Owner", keyholder = "Key holder",
|
||||
openDetails = "Open property details", tryAgain = "Try Again", refresh = "Refresh",
|
||||
offline = "House is offline", offlineBody = "Start a supported housing resource or review Config.Housing.",
|
||||
unavailable = "Homes unavailable", empty = "No Homes", emptyBody = "Owned homes and homes shared with you will appear here.",
|
||||
provider = "Housing data · {system}", access = "Access", status = "Status",
|
||||
actions = "Controls", setWaypoint = "Directions", viewCamera = "View Camera",
|
||||
garage = "Garage", garageEnabled = "Enabled", garageDisabled = "Unavailable",
|
||||
storedVehicles = "{count} stored", camera = "Camera", cameraAvailable = "Ready", cameraUnavailable = "Unavailable",
|
||||
keys = "Keys", manageKeys = "Manage Keys", addKey = "Add Key", revokeKey = "Revoke",
|
||||
noKeys = "No shared keys", noKeysBody = "Only you currently have access to this home.",
|
||||
chooseResident = "Give a Key", chooseResidentBody = "Choose a person currently inside this property.",
|
||||
noCandidates = "Nobody is waiting inside", noCandidatesBody = "A person must be inside the property before you can give them a key.",
|
||||
revokeTitle = "Revoke Key?", revokeBody = "{name} will lose access to this property.",
|
||||
offlineKey = "Offline", onlineKey = "Online", cancel = "Cancel", confirm = "Confirm",
|
||||
lockSuccess = "Door status updated.", waypointSuccess = "Route set on your map.",
|
||||
keyGranted = "Key granted.", keyRevoked = "Key revoked.", cameraStarting = "Opening camera…",
|
||||
errors = {
|
||||
provider_unavailable = "The housing provider is unavailable.", provider_error = "The housing provider could not be reached.",
|
||||
housing_unavailable = "Housing data is unavailable for this character.", property_not_found = "This property no longer exists.",
|
||||
property_access_denied = "You no longer have access to this property.", owner_required = "Only the property owner can do that.",
|
||||
invalid_property = "Select a valid property.", invalid_action = "This housing action is unavailable.", capability_unavailable = "This property provider does not support that control.",
|
||||
invalid_target = "Select a valid resident.", target_not_in_property = "That person is no longer inside the property.",
|
||||
key_not_found = "That key no longer exists.", key_already_exists = "That person already has access to this property.", keyholder_offline = "Offline keys must be revoked through the property menu.",
|
||||
cctv_unavailable = "The camera is unavailable for this property.", provider_rejected = "The housing provider rejected the request.",
|
||||
action_failed = "The property change could not be saved.", rate_limited = "Please wait before using another housing control.", device_not_open = "Open the phone again to manage your homes.",
|
||||
device_locked = "Unlock the phone to manage your homes.", request_failed = "The housing request failed.", default = "The housing request failed.",
|
||||
},
|
||||
},
|
||||
calculator = { name = "Calculator" },
|
||||
snake = {
|
||||
|
||||
@@ -24,10 +24,13 @@ client_scripts {
|
||||
'config/locales/*.lua',
|
||||
'source/bridge/client/framework.lua',
|
||||
'source/bridge/client/callbacks.lua',
|
||||
'source/bridge/client/housing.lua',
|
||||
'source/bridge/client/housing/*.lua',
|
||||
'source/client/animations.lua',
|
||||
'source/client/camera.lua',
|
||||
'source/client/garage.lua',
|
||||
'source/client/skyride.lua',
|
||||
'source/client/housing.lua',
|
||||
'source/bridge/client/radio.lua',
|
||||
'source/client/main.lua',
|
||||
'source/client/radio.lua',
|
||||
@@ -43,6 +46,8 @@ server_scripts {
|
||||
'source/bridge/server/callbacks.lua',
|
||||
'source/bridge/server/framework.lua',
|
||||
'source/bridge/server/frameworks/*.lua',
|
||||
'source/bridge/server/housing.lua',
|
||||
'source/bridge/server/housing/*.lua',
|
||||
'source/bridge/server/inventory.lua',
|
||||
'source/bridge/server/inventory/*.lua',
|
||||
'source/server/db_migrate.lua',
|
||||
@@ -57,6 +62,7 @@ server_scripts {
|
||||
'source/server/mail.lua',
|
||||
'source/server/banking.lua',
|
||||
'source/server/garage.lua',
|
||||
'source/server/housing.lua',
|
||||
'source/server/marketplace.lua',
|
||||
'source/server/pages.lua',
|
||||
'source/server/fliptok.lua',
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
Bridge.Housing = {
|
||||
ClientProviders = {},
|
||||
}
|
||||
|
||||
function Bridge.Housing.RegisterClientProvider(name, provider)
|
||||
assert(type(name) == "string" and name ~= "", "Housing provider name must be a non-empty string")
|
||||
assert(type(provider) == "table" and type(provider.execute) == "function", "Housing client provider must implement execute")
|
||||
assert(not Bridge.Housing.ClientProviders[name], ("Housing client provider '%s' is already registered"):format(name))
|
||||
Bridge.Housing.ClientProviders[name] = provider
|
||||
end
|
||||
|
||||
function Bridge.Housing.Execute(provider_name, action, data)
|
||||
local provider = Bridge.Housing.ClientProviders[provider_name]
|
||||
if not provider then
|
||||
return false, "provider_unavailable"
|
||||
end
|
||||
return provider.execute(action, data)
|
||||
end
|
||||
@@ -0,0 +1,63 @@
|
||||
if Bridge.Framework.GetName() ~= "esx" then
|
||||
return
|
||||
end
|
||||
|
||||
local ESX = exports["es_extended"]:getSharedObject()
|
||||
|
||||
local callback_names = {
|
||||
grant_key = "esx_property:GiveKey",
|
||||
revoke_key = "esx_property:RemoveKey",
|
||||
toggle_lock = "esx_property:toggleLock",
|
||||
}
|
||||
|
||||
local function await_callback(name, arguments)
|
||||
local request = promise.new()
|
||||
local settled = false
|
||||
ESX.TriggerServerCallback(name, function(success)
|
||||
if settled then
|
||||
return
|
||||
end
|
||||
settled = true
|
||||
request:resolve(success and true or false)
|
||||
end, table.unpack(arguments))
|
||||
SetTimeout(Config.Bridge.CallbackTimeout, function()
|
||||
if settled then
|
||||
return
|
||||
end
|
||||
settled = true
|
||||
Bridge.Debug("error", "[sky_phone] Housing provider callback '%s' timed out.", name)
|
||||
request:resolve(false)
|
||||
end)
|
||||
return Citizen.Await(request)
|
||||
end
|
||||
|
||||
Bridge.Housing.RegisterClientProvider("esx_property", {
|
||||
execute = function(action, data)
|
||||
local callback_name = callback_names[action]
|
||||
if not callback_name or GetResourceState("esx_property") ~= "started" then
|
||||
return false, "provider_unavailable"
|
||||
end
|
||||
|
||||
local property_id = tonumber(data.providerId)
|
||||
if not property_id or property_id < 1 or property_id ~= math.floor(property_id) then
|
||||
return false, "invalid_property"
|
||||
end
|
||||
|
||||
local arguments = { property_id }
|
||||
if action == "grant_key" then
|
||||
arguments[2] = tonumber(data.target)
|
||||
elseif action == "revoke_key" then
|
||||
arguments[2] = data.identifier
|
||||
end
|
||||
if (action == "grant_key" and not arguments[2])
|
||||
or (action == "revoke_key" and type(arguments[2]) ~= "string")
|
||||
then
|
||||
return false, "invalid_request"
|
||||
end
|
||||
|
||||
if await_callback(callback_name, arguments) then
|
||||
return true
|
||||
end
|
||||
return false, "provider_rejected"
|
||||
end,
|
||||
})
|
||||
@@ -0,0 +1,24 @@
|
||||
if Bridge.Framework.GetName() ~= "qbox" then
|
||||
return
|
||||
end
|
||||
|
||||
Bridge.Housing.RegisterClientProvider("qbx_properties", {
|
||||
execute = function(action, data)
|
||||
if action ~= "grant_key" and action ~= "revoke_key" then
|
||||
return false, "capability_unavailable"
|
||||
end
|
||||
if GetResourceState("qbx_properties") ~= "started" then
|
||||
return false, "provider_unavailable"
|
||||
end
|
||||
local result = Bridge.Callbacks.Trigger("sky_phone:housing:qbx_properties:execute", {
|
||||
action = action,
|
||||
providerId = data.providerId,
|
||||
target = data.target,
|
||||
identifier = data.identifier,
|
||||
})
|
||||
if result and result.success then
|
||||
return true
|
||||
end
|
||||
return false, result and result.error or "provider_rejected"
|
||||
end,
|
||||
})
|
||||
@@ -0,0 +1,113 @@
|
||||
Bridge.Housing = {
|
||||
Providers = {},
|
||||
}
|
||||
|
||||
local selected_provider_name = nil
|
||||
local last_provider_state = nil
|
||||
|
||||
function Bridge.Housing.RegisterProvider(name, provider)
|
||||
assert(type(name) == "string" and name ~= "", "Housing provider name must be a non-empty string")
|
||||
assert(type(provider) == "table", "Housing provider must be a table")
|
||||
assert(type(provider.is_available) == "function", "Housing provider must implement is_available")
|
||||
assert(type(provider.get_overview) == "function", "Housing provider must implement get_overview")
|
||||
assert(type(provider.prepare) == "function", "Housing provider must implement prepare")
|
||||
assert(not Bridge.Housing.Providers[name], ("Housing provider '%s' is already registered"):format(name))
|
||||
Bridge.Housing.Providers[name] = provider
|
||||
end
|
||||
|
||||
local function provider_available(name)
|
||||
local provider = Bridge.Housing.Providers[name]
|
||||
return provider and provider.is_available()
|
||||
end
|
||||
|
||||
local function report_provider_state(state, message)
|
||||
if last_provider_state == state then
|
||||
return
|
||||
end
|
||||
last_provider_state = state
|
||||
Bridge.Debug(state == "online" and "debug" or "warn", message, { always = true })
|
||||
end
|
||||
|
||||
function Bridge.Housing.ResolveProvider()
|
||||
if selected_provider_name and provider_available(selected_provider_name) then
|
||||
return selected_provider_name, Bridge.Housing.Providers[selected_provider_name]
|
||||
end
|
||||
|
||||
selected_provider_name = nil
|
||||
local configured = tostring(Config.Housing.System or "auto")
|
||||
if configured ~= "auto" then
|
||||
if provider_available(configured) then
|
||||
selected_provider_name = configured
|
||||
else
|
||||
report_provider_state(
|
||||
"offline:" .. configured,
|
||||
("[sky_phone] Configured housing provider '%s' is unavailable."):format(configured)
|
||||
)
|
||||
return nil, nil
|
||||
end
|
||||
else
|
||||
for _, name in ipairs(Config.Housing.AutoPriority or {}) do
|
||||
if provider_available(name) then
|
||||
selected_provider_name = name
|
||||
break
|
||||
end
|
||||
end
|
||||
if not selected_provider_name then
|
||||
report_provider_state("offline:auto", "[sky_phone] No supported housing provider is running.")
|
||||
return nil, nil
|
||||
end
|
||||
end
|
||||
|
||||
report_provider_state(
|
||||
"online",
|
||||
("[sky_phone] Housing provider '%s' is active."):format(selected_provider_name)
|
||||
)
|
||||
return selected_provider_name, Bridge.Housing.Providers[selected_provider_name]
|
||||
end
|
||||
|
||||
function Bridge.Housing.GetOverview(source)
|
||||
local name, provider = Bridge.Housing.ResolveProvider()
|
||||
if not provider then
|
||||
return {
|
||||
available = false,
|
||||
provider = nil,
|
||||
properties = {},
|
||||
}
|
||||
end
|
||||
|
||||
local properties, error_code = provider.get_overview(source)
|
||||
if not properties then
|
||||
return nil, error_code or "provider_error"
|
||||
end
|
||||
return {
|
||||
available = true,
|
||||
provider = name,
|
||||
properties = properties,
|
||||
}
|
||||
end
|
||||
|
||||
function Bridge.Housing.Prepare(source, action, data)
|
||||
local name, provider = Bridge.Housing.ResolveProvider()
|
||||
if not provider then
|
||||
return nil, "provider_unavailable"
|
||||
end
|
||||
local prepared, error_code = provider.prepare(source, action, data)
|
||||
if not prepared then
|
||||
return nil, error_code or "action_failed"
|
||||
end
|
||||
prepared.provider = name
|
||||
return prepared
|
||||
end
|
||||
|
||||
local function reset_provider(resource_name)
|
||||
for _, provider in pairs(Bridge.Housing.Providers) do
|
||||
if provider.resource_name == resource_name then
|
||||
selected_provider_name = nil
|
||||
last_provider_state = nil
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
AddEventHandler("onResourceStart", reset_provider)
|
||||
AddEventHandler("onResourceStop", reset_provider)
|
||||
@@ -0,0 +1,272 @@
|
||||
local resource_name = "esx_property"
|
||||
|
||||
local function property_list()
|
||||
local success, properties = pcall(function()
|
||||
return exports[resource_name]:GetProperties()
|
||||
end)
|
||||
if not success or type(properties) ~= "table" then
|
||||
Bridge.Debug("error", "[sky_phone] esx_property:GetProperties failed: %s", tostring(properties))
|
||||
return nil
|
||||
end
|
||||
return properties
|
||||
end
|
||||
|
||||
local function online_source(identifier)
|
||||
if type(identifier) ~= "string" or identifier == "" then
|
||||
return nil
|
||||
end
|
||||
for _, player_source in ipairs(Bridge.Framework.GetPlayers()) do
|
||||
if Bridge.Framework.GetIdentifier(player_source) == identifier then
|
||||
return player_source
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function player_name(player_source)
|
||||
local first_name = Bridge.Framework.GetFirstname(player_source)
|
||||
local last_name = Bridge.Framework.GetLastname(player_source)
|
||||
local name = table.concat({ tostring(first_name or ""), tostring(last_name or "") }, " ")
|
||||
:match("^%s*(.-)%s*$")
|
||||
if name ~= "" then
|
||||
return name
|
||||
end
|
||||
return GetPlayerName(player_source) or tostring(player_source)
|
||||
end
|
||||
|
||||
local function valid_coords(value)
|
||||
return type(value) == "table"
|
||||
and tonumber(value.x) ~= nil
|
||||
and tonumber(value.y) ~= nil
|
||||
and tonumber(value.z) ~= nil
|
||||
end
|
||||
|
||||
local function valid_camera(property)
|
||||
local cctv = property.cctv
|
||||
return type(cctv) == "table"
|
||||
and cctv.enabled == true
|
||||
and valid_coords(property.Entrance)
|
||||
and valid_coords(cctv.rot)
|
||||
end
|
||||
|
||||
local function access_for(property, identifier)
|
||||
if property.Owned ~= true then
|
||||
return nil
|
||||
end
|
||||
if property.Owner == identifier then
|
||||
return "owner"
|
||||
end
|
||||
if type(property.Keys) == "table" and property.Keys[identifier] then
|
||||
return "keyholder"
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function normalized_keys(property)
|
||||
local keys = {}
|
||||
if type(property.Keys) ~= "table" then
|
||||
return keys
|
||||
end
|
||||
for identifier, value in pairs(property.Keys) do
|
||||
if type(identifier) == "string" and type(value) == "table" then
|
||||
local online = online_source(identifier) ~= nil
|
||||
keys[#keys + 1] = {
|
||||
identifier = identifier,
|
||||
name = tostring(value.name or identifier),
|
||||
online = online,
|
||||
revocable = online,
|
||||
}
|
||||
end
|
||||
end
|
||||
table.sort(keys, function(left, right)
|
||||
return string.lower(left.name) < string.lower(right.name)
|
||||
end)
|
||||
return keys
|
||||
end
|
||||
|
||||
local function normalized_property(index, property, access)
|
||||
local entrance = property.Entrance
|
||||
local garage = type(property.garage) == "table" and property.garage or nil
|
||||
local stored_vehicles = garage and type(garage.StoredVehicles) == "table"
|
||||
and #garage.StoredVehicles or 0
|
||||
return {
|
||||
id = ("esx_property:%s"):format(index),
|
||||
providerId = tostring(index),
|
||||
name = tostring(property.setName and property.setName ~= "" and property.setName or property.Name or ("Property %s"):format(index)),
|
||||
access = access,
|
||||
locked = property.Locked == true,
|
||||
entrance = {
|
||||
x = tonumber(entrance.x),
|
||||
y = tonumber(entrance.y),
|
||||
z = tonumber(entrance.z),
|
||||
},
|
||||
capabilities = {
|
||||
lock = true,
|
||||
keys = access == "owner",
|
||||
waypoint = true,
|
||||
cctv = valid_camera(property),
|
||||
garageStatus = garage ~= nil,
|
||||
},
|
||||
cctv = {
|
||||
enabled = valid_camera(property),
|
||||
},
|
||||
garage = garage and {
|
||||
enabled = garage.enabled == true,
|
||||
storedVehicles = stored_vehicles,
|
||||
} or nil,
|
||||
keys = access == "owner" and normalized_keys(property) or nil,
|
||||
}
|
||||
end
|
||||
|
||||
local function resolve_property(source, data)
|
||||
if type(data) ~= "table" or type(data.propertyId) ~= "string" then
|
||||
return nil, nil, nil, "invalid_request"
|
||||
end
|
||||
local provider_id = data.propertyId:match("^esx_property:(%d+)$")
|
||||
local index = tonumber(provider_id)
|
||||
if not index or index < 1 or index ~= math.floor(index) then
|
||||
return nil, nil, nil, "invalid_property"
|
||||
end
|
||||
local properties = property_list()
|
||||
local property = properties and properties[index]
|
||||
if type(property) ~= "table" or not valid_coords(property.Entrance) then
|
||||
return nil, nil, nil, "property_not_found"
|
||||
end
|
||||
local identifier = Bridge.Framework.GetIdentifier(source)
|
||||
local access = identifier and access_for(property, identifier) or nil
|
||||
if not access then
|
||||
return nil, nil, nil, "property_access_denied"
|
||||
end
|
||||
return property, tostring(index), access
|
||||
end
|
||||
|
||||
local function key_candidates(source, property)
|
||||
local candidates = {}
|
||||
local seen = {}
|
||||
for _, value in pairs(property.plysinside or {}) do
|
||||
local target = tonumber(value)
|
||||
local target_identifier = target and Bridge.Framework.GetIdentifier(target) or nil
|
||||
if target and target ~= source and target_identifier and not seen[target]
|
||||
and target_identifier ~= property.Owner
|
||||
and not (type(property.Keys) == "table" and property.Keys[target_identifier])
|
||||
then
|
||||
seen[target] = true
|
||||
candidates[#candidates + 1] = {
|
||||
id = target,
|
||||
name = player_name(target),
|
||||
}
|
||||
end
|
||||
end
|
||||
table.sort(candidates, function(left, right)
|
||||
return string.lower(left.name) < string.lower(right.name)
|
||||
end)
|
||||
return candidates
|
||||
end
|
||||
|
||||
Bridge.Housing.RegisterProvider("esx_property", {
|
||||
resource_name = resource_name,
|
||||
is_available = function()
|
||||
return Bridge.Framework.GetName() == "esx"
|
||||
and GetResourceState(resource_name) == "started"
|
||||
end,
|
||||
get_overview = function(source)
|
||||
local identifier = Bridge.Framework.GetIdentifier(source)
|
||||
if not identifier then
|
||||
return nil, "housing_unavailable"
|
||||
end
|
||||
local properties = property_list()
|
||||
if not properties then
|
||||
return nil, "provider_error"
|
||||
end
|
||||
local result = {}
|
||||
for index, property in ipairs(properties) do
|
||||
local access = type(property) == "table" and access_for(property, identifier) or nil
|
||||
if access and valid_coords(property.Entrance) then
|
||||
result[#result + 1] = normalized_property(index, property, access)
|
||||
if #result >= Config.Housing.MaximumProperties then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(result, function(left, right)
|
||||
if left.access ~= right.access then
|
||||
return left.access == "owner"
|
||||
end
|
||||
return string.lower(left.name) < string.lower(right.name)
|
||||
end)
|
||||
return result
|
||||
end,
|
||||
prepare = function(source, action, data)
|
||||
local property, provider_id, access, error_code = resolve_property(source, data)
|
||||
if not property then
|
||||
return nil, error_code
|
||||
end
|
||||
|
||||
if action == "key_candidates" then
|
||||
if access ~= "owner" then
|
||||
return nil, "owner_required"
|
||||
end
|
||||
return { candidates = key_candidates(source, property) }
|
||||
end
|
||||
if action == "toggle_lock" then
|
||||
return { providerId = provider_id }
|
||||
end
|
||||
if action == "set_waypoint" then
|
||||
return {
|
||||
coords = {
|
||||
x = tonumber(property.Entrance.x),
|
||||
y = tonumber(property.Entrance.y),
|
||||
z = tonumber(property.Entrance.z),
|
||||
},
|
||||
}
|
||||
end
|
||||
if action == "open_cctv" then
|
||||
if not valid_camera(property) then
|
||||
return nil, "cctv_unavailable"
|
||||
end
|
||||
return {
|
||||
camera = {
|
||||
coords = {
|
||||
x = tonumber(property.Entrance.x),
|
||||
y = tonumber(property.Entrance.y),
|
||||
z = tonumber(property.Entrance.z) + Config.Housing.Camera.HeightAboveEntrance,
|
||||
},
|
||||
rotation = {
|
||||
x = tonumber(property.cctv.rot.x),
|
||||
y = tonumber(property.cctv.rot.y),
|
||||
z = tonumber(property.cctv.rot.z),
|
||||
},
|
||||
maximumLeft = tonumber(property.cctv.maxleft),
|
||||
maximumRight = tonumber(property.cctv.maxright),
|
||||
},
|
||||
}
|
||||
end
|
||||
if access ~= "owner" then
|
||||
return nil, "owner_required"
|
||||
end
|
||||
if action == "grant_key" then
|
||||
local target = tonumber(data.target)
|
||||
if not target then
|
||||
return nil, "invalid_target"
|
||||
end
|
||||
local candidates = key_candidates(source, property)
|
||||
for _, candidate in ipairs(candidates) do
|
||||
if candidate.id == target then
|
||||
return { providerId = provider_id, target = target }
|
||||
end
|
||||
end
|
||||
return nil, "target_not_in_property"
|
||||
end
|
||||
if action == "revoke_key" then
|
||||
local identifier = data.identifier
|
||||
if type(identifier) ~= "string" or type(property.Keys) ~= "table" or not property.Keys[identifier] then
|
||||
return nil, "key_not_found"
|
||||
end
|
||||
if not online_source(identifier) then
|
||||
return nil, "keyholder_offline"
|
||||
end
|
||||
return { providerId = provider_id, identifier = identifier }
|
||||
end
|
||||
return nil, "invalid_action"
|
||||
end,
|
||||
})
|
||||
@@ -0,0 +1,369 @@
|
||||
local resource_name = "qbx_properties"
|
||||
|
||||
local function decode_array(value)
|
||||
if type(value) == "table" then
|
||||
return value
|
||||
end
|
||||
if type(value) ~= "string" or value == "" then
|
||||
return {}
|
||||
end
|
||||
local success, decoded = pcall(json.decode, value)
|
||||
return success and type(decoded) == "table" and decoded or {}
|
||||
end
|
||||
|
||||
local function query_properties(identifier)
|
||||
local success, properties = pcall(function()
|
||||
return MySQL.query.await([[
|
||||
SELECT id, property_name, owner, keyholders, coords, garage
|
||||
FROM properties
|
||||
WHERE owner = ? OR JSON_CONTAINS(keyholders, JSON_QUOTE(?))
|
||||
ORDER BY property_name ASC, id ASC
|
||||
]], { identifier, identifier })
|
||||
end)
|
||||
if not success or type(properties) ~= "table" then
|
||||
Bridge.Debug("error", "[sky_phone] qbx_properties overview query failed: %s", tostring(properties))
|
||||
return nil
|
||||
end
|
||||
return properties
|
||||
end
|
||||
|
||||
local function query_property(property_id)
|
||||
local success, property = pcall(function()
|
||||
return MySQL.single.await([[
|
||||
SELECT id, property_name, owner, keyholders, coords, garage
|
||||
FROM properties
|
||||
WHERE id = ?
|
||||
]], { property_id })
|
||||
end)
|
||||
if not success then
|
||||
Bridge.Debug("error", "[sky_phone] qbx_properties property query failed: %s", tostring(property))
|
||||
return nil, "provider_error"
|
||||
end
|
||||
if type(property) ~= "table" then
|
||||
return nil, "property_not_found"
|
||||
end
|
||||
return property
|
||||
end
|
||||
|
||||
local function online_source(identifier)
|
||||
for _, player_source in ipairs(Bridge.Framework.GetPlayers()) do
|
||||
if Bridge.Framework.GetIdentifier(player_source) == identifier then
|
||||
return player_source
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function character_name(identifier)
|
||||
local player = exports.qbx_core:GetPlayerByCitizenId(identifier)
|
||||
or exports.qbx_core:GetOfflinePlayer(identifier)
|
||||
local character = player and player.PlayerData and player.PlayerData.charinfo
|
||||
if character then
|
||||
local name = ("%s %s"):format(character.firstname or "", character.lastname or "")
|
||||
:match("^%s*(.-)%s*$")
|
||||
if name ~= "" then
|
||||
return name
|
||||
end
|
||||
end
|
||||
return identifier
|
||||
end
|
||||
|
||||
local function access_for(property, identifier)
|
||||
if property.owner == identifier then
|
||||
return "owner"
|
||||
end
|
||||
for _, keyholder in ipairs(decode_array(property.keyholders)) do
|
||||
if keyholder == identifier then
|
||||
return "keyholder"
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function valid_coords(value)
|
||||
return type(value) == "table"
|
||||
and tonumber(value.x) ~= nil
|
||||
and tonumber(value.y) ~= nil
|
||||
and tonumber(value.z) ~= nil
|
||||
end
|
||||
|
||||
local function garage_name(property_name)
|
||||
return "property_" .. string.gsub(string.lower(property_name), " ", "_")
|
||||
end
|
||||
|
||||
local function garage_counts(identifier)
|
||||
if GetResourceState("qbx_garages") ~= "started" then
|
||||
return {}
|
||||
end
|
||||
local success, rows = pcall(function()
|
||||
return MySQL.query.await([[
|
||||
SELECT garage, COUNT(*) AS stored_count
|
||||
FROM player_vehicles
|
||||
WHERE citizenid = ? AND state = 1 AND garage IS NOT NULL
|
||||
GROUP BY garage
|
||||
]], { identifier })
|
||||
end)
|
||||
if not success or type(rows) ~= "table" then
|
||||
Bridge.Debug("error", "[sky_phone] qbx_properties garage status query failed: %s", tostring(rows))
|
||||
return {}
|
||||
end
|
||||
local counts = {}
|
||||
for _, row in ipairs(rows) do
|
||||
if type(row.garage) == "string" then
|
||||
counts[row.garage] = tonumber(row.stored_count) or 0
|
||||
end
|
||||
end
|
||||
return counts
|
||||
end
|
||||
|
||||
local function normalized_keys(property)
|
||||
local keys = {}
|
||||
for _, identifier in ipairs(decode_array(property.keyholders)) do
|
||||
if type(identifier) == "string" and identifier ~= "" then
|
||||
keys[#keys + 1] = {
|
||||
identifier = identifier,
|
||||
name = character_name(identifier),
|
||||
online = online_source(identifier) ~= nil,
|
||||
revocable = true,
|
||||
}
|
||||
end
|
||||
end
|
||||
table.sort(keys, function(left, right)
|
||||
return string.lower(left.name) < string.lower(right.name)
|
||||
end)
|
||||
return keys
|
||||
end
|
||||
|
||||
local function normalized_property(property, access, counts)
|
||||
local coords = decode_array(property.coords)
|
||||
local garage = property.garage and decode_array(property.garage) or nil
|
||||
local garage_configured = garage and valid_coords(garage)
|
||||
local garage_enabled = garage_configured
|
||||
and GetResourceState("qbx_garages") == "started"
|
||||
return {
|
||||
id = ("qbx_properties:%s"):format(property.id),
|
||||
providerId = tostring(property.id),
|
||||
name = tostring(property.property_name or ("Property %s"):format(property.id)),
|
||||
access = access,
|
||||
locked = false,
|
||||
entrance = {
|
||||
x = tonumber(coords.x),
|
||||
y = tonumber(coords.y),
|
||||
z = tonumber(coords.z),
|
||||
},
|
||||
capabilities = {
|
||||
lock = false,
|
||||
keys = access == "owner",
|
||||
waypoint = true,
|
||||
cctv = false,
|
||||
garageStatus = garage_configured and true or false,
|
||||
},
|
||||
cctv = { enabled = false },
|
||||
garage = garage_configured and {
|
||||
enabled = garage_enabled and true or false,
|
||||
storedVehicles = counts[garage_name(property.property_name)] or 0,
|
||||
} or nil,
|
||||
keys = access == "owner" and normalized_keys(property) or nil,
|
||||
}
|
||||
end
|
||||
|
||||
local function parse_property_id(value)
|
||||
if type(value) ~= "string" then
|
||||
return nil
|
||||
end
|
||||
local property_id = tonumber(value:match("^qbx_properties:(%d+)$"))
|
||||
if not property_id or property_id < 1 or property_id ~= math.floor(property_id) then
|
||||
return nil
|
||||
end
|
||||
return property_id
|
||||
end
|
||||
|
||||
local function resolve_property(source, data)
|
||||
local property_id = parse_property_id(data and data.propertyId)
|
||||
if not property_id then
|
||||
return nil, nil, nil, "invalid_property"
|
||||
end
|
||||
local property, error_code = query_property(property_id)
|
||||
if not property then
|
||||
return nil, nil, nil, error_code
|
||||
end
|
||||
local identifier = Bridge.Framework.GetIdentifier(source)
|
||||
local access = identifier and access_for(property, identifier) or nil
|
||||
local coords = decode_array(property.coords)
|
||||
if not access then
|
||||
return nil, nil, nil, "property_access_denied"
|
||||
end
|
||||
if not valid_coords(coords) then
|
||||
return nil, nil, nil, "invalid_coordinates"
|
||||
end
|
||||
return property, property_id, access
|
||||
end
|
||||
|
||||
local function player_property_id(player)
|
||||
local metadata = player and player.PlayerData and player.PlayerData.metadata
|
||||
return metadata and tonumber(metadata.currentPropertyId) or nil
|
||||
end
|
||||
|
||||
local function key_candidates(source, property, property_id)
|
||||
local candidates = {}
|
||||
for _, target in ipairs(Bridge.Framework.GetPlayers()) do
|
||||
local player = exports.qbx_core:GetPlayer(target)
|
||||
local identifier = player and player.PlayerData and player.PlayerData.citizenid
|
||||
if target ~= source and identifier and player_property_id(player) == property_id
|
||||
and not access_for(property, identifier)
|
||||
then
|
||||
candidates[#candidates + 1] = {
|
||||
id = target,
|
||||
name = character_name(identifier),
|
||||
}
|
||||
end
|
||||
end
|
||||
table.sort(candidates, function(left, right)
|
||||
return string.lower(left.name) < string.lower(right.name)
|
||||
end)
|
||||
return candidates
|
||||
end
|
||||
|
||||
local function execute_key_action(source, action, data)
|
||||
if not SkyPhone.AllowOperation(source, "housing_qbx_action", Config.Housing.ActionsPerMinute, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
end
|
||||
local session, error_response = SkyPhone.RequireSession(source)
|
||||
if not session then
|
||||
return error_response
|
||||
end
|
||||
if action ~= "grant_key" and action ~= "revoke_key" then
|
||||
return { success = false, error = "invalid_action" }
|
||||
end
|
||||
|
||||
local property_id = tonumber(data and data.providerId)
|
||||
if not property_id or property_id < 1 or property_id ~= math.floor(property_id) then
|
||||
return { success = false, error = "invalid_property" }
|
||||
end
|
||||
local property, error_code = query_property(property_id)
|
||||
if not property then
|
||||
return { success = false, error = error_code }
|
||||
end
|
||||
local identifier = Bridge.Framework.GetIdentifier(source)
|
||||
if not identifier or property.owner ~= identifier then
|
||||
return { success = false, error = "owner_required" }
|
||||
end
|
||||
|
||||
if action == "grant_key" then
|
||||
local target = tonumber(data.target)
|
||||
local target_player = target and exports.qbx_core:GetPlayer(target) or nil
|
||||
local target_identifier = target_player and target_player.PlayerData
|
||||
and target_player.PlayerData.citizenid or nil
|
||||
if not target_identifier or target == source or player_property_id(target_player) ~= property_id then
|
||||
return { success = false, error = "target_not_in_property" }
|
||||
end
|
||||
if access_for(property, target_identifier) then
|
||||
return { success = false, error = "key_already_exists" }
|
||||
end
|
||||
local affected = MySQL.update.await([[
|
||||
UPDATE properties
|
||||
SET keyholders = JSON_ARRAY_APPEND(
|
||||
CASE WHEN JSON_TYPE(keyholders) = 'ARRAY' THEN keyholders ELSE JSON_ARRAY() END,
|
||||
'$', ?
|
||||
)
|
||||
WHERE id = ? AND owner = ?
|
||||
AND NOT JSON_CONTAINS(
|
||||
CASE WHEN JSON_TYPE(keyholders) = 'ARRAY' THEN keyholders ELSE JSON_ARRAY() END,
|
||||
JSON_QUOTE(?)
|
||||
)
|
||||
]], { target_identifier, property_id, identifier, target_identifier })
|
||||
return affected == 1 and { success = true } or { success = false, error = "action_failed" }
|
||||
end
|
||||
|
||||
local target_identifier = data.identifier
|
||||
if type(target_identifier) ~= "string" or target_identifier == "" then
|
||||
return { success = false, error = "invalid_target" }
|
||||
end
|
||||
local affected = MySQL.update.await([[
|
||||
UPDATE properties
|
||||
SET keyholders = JSON_REMOVE(
|
||||
keyholders,
|
||||
JSON_UNQUOTE(JSON_SEARCH(keyholders, 'one', ?))
|
||||
)
|
||||
WHERE id = ? AND owner = ?
|
||||
AND JSON_SEARCH(keyholders, 'one', ?) IS NOT NULL
|
||||
]], { target_identifier, property_id, identifier, target_identifier })
|
||||
return affected == 1 and { success = true } or { success = false, error = "key_not_found" }
|
||||
end
|
||||
|
||||
Bridge.Housing.RegisterProvider("qbx_properties", {
|
||||
resource_name = resource_name,
|
||||
is_available = function()
|
||||
return Bridge.Framework.GetName() == "qbox"
|
||||
and GetResourceState(resource_name) == "started"
|
||||
end,
|
||||
get_overview = function(source)
|
||||
local identifier = Bridge.Framework.GetIdentifier(source)
|
||||
if not identifier then
|
||||
return nil, "housing_unavailable"
|
||||
end
|
||||
local properties = query_properties(identifier)
|
||||
if not properties then
|
||||
return nil, "provider_error"
|
||||
end
|
||||
local counts = garage_counts(identifier)
|
||||
local result = {}
|
||||
for _, property in ipairs(properties) do
|
||||
local access = access_for(property, identifier)
|
||||
local coords = decode_array(property.coords)
|
||||
if access and valid_coords(coords) then
|
||||
result[#result + 1] = normalized_property(property, access, counts)
|
||||
if #result >= Config.Housing.MaximumProperties then
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(result, function(left, right)
|
||||
if left.access ~= right.access then
|
||||
return left.access == "owner"
|
||||
end
|
||||
return string.lower(left.name) < string.lower(right.name)
|
||||
end)
|
||||
return result
|
||||
end,
|
||||
prepare = function(source, action, data)
|
||||
local property, property_id, access, error_code = resolve_property(source, data)
|
||||
if not property then
|
||||
return nil, error_code
|
||||
end
|
||||
if action == "set_waypoint" then
|
||||
local coords = decode_array(property.coords)
|
||||
return { coords = { x = tonumber(coords.x), y = tonumber(coords.y), z = tonumber(coords.z) } }
|
||||
end
|
||||
if action == "key_candidates" then
|
||||
if access ~= "owner" then
|
||||
return nil, "owner_required"
|
||||
end
|
||||
return { candidates = key_candidates(source, property, property_id) }
|
||||
end
|
||||
if action == "grant_key" or action == "revoke_key" then
|
||||
if access ~= "owner" then
|
||||
return nil, "owner_required"
|
||||
end
|
||||
return {
|
||||
providerId = tostring(property_id),
|
||||
target = data.target,
|
||||
identifier = data.identifier,
|
||||
}
|
||||
end
|
||||
if action == "toggle_lock" then
|
||||
return nil, "capability_unavailable"
|
||||
end
|
||||
if action == "open_cctv" then
|
||||
return nil, "cctv_unavailable"
|
||||
end
|
||||
return nil, "invalid_action"
|
||||
end,
|
||||
})
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:housing:qbx_properties:execute", function(source, data)
|
||||
if type(data) ~= "table" or type(data.action) ~= "string" then
|
||||
return { success = false, error = "invalid_request" }
|
||||
end
|
||||
return execute_key_action(source, data.action, data)
|
||||
end)
|
||||
@@ -0,0 +1,219 @@
|
||||
local camera_state = nil
|
||||
|
||||
local function server_prepare(action, data)
|
||||
local request = {
|
||||
action = action,
|
||||
propertyId = data and data.propertyId,
|
||||
target = data and data.target,
|
||||
identifier = data and data.identifier,
|
||||
}
|
||||
return Bridge.Callbacks.Trigger("sky_phone:housing:prepare", request)
|
||||
end
|
||||
|
||||
local function suspend_phone()
|
||||
SetNuiFocus(false, false)
|
||||
TriggerEvent("sky_phone:animation:phone", false)
|
||||
SendNUIMessage({ type = "app:suspend" })
|
||||
end
|
||||
|
||||
local function resume_phone()
|
||||
SendNUIMessage({ type = "app:resume" })
|
||||
SetNuiFocus(true, true)
|
||||
TriggerEvent("sky_phone:animation:phone", true)
|
||||
end
|
||||
|
||||
local function stop_camera()
|
||||
local state = camera_state
|
||||
if not state then
|
||||
return
|
||||
end
|
||||
camera_state = nil
|
||||
RenderScriptCams(false, true, 350, true, true)
|
||||
if state.camera and DoesCamExist(state.camera) then
|
||||
DestroyCam(state.camera, false)
|
||||
end
|
||||
ClearFocus()
|
||||
SetNightvision(state.night_vision)
|
||||
if DoesEntityExist(state.ped) then
|
||||
FreezeEntityPosition(state.ped, state.frozen)
|
||||
end
|
||||
resume_phone()
|
||||
end
|
||||
|
||||
local function camera_number(value, fallback)
|
||||
local number = tonumber(value)
|
||||
if not number or number ~= number or number == math.huge or number == -math.huge then
|
||||
return fallback
|
||||
end
|
||||
return number
|
||||
end
|
||||
|
||||
local function run_camera(provider_name, camera_data)
|
||||
if camera_state or type(camera_data) ~= "table"
|
||||
or type(camera_data.coords) ~= "table"
|
||||
or type(camera_data.rotation) ~= "table"
|
||||
then
|
||||
return
|
||||
end
|
||||
local coords = {
|
||||
x = camera_number(camera_data.coords.x),
|
||||
y = camera_number(camera_data.coords.y),
|
||||
z = camera_number(camera_data.coords.z),
|
||||
}
|
||||
local rotation = {
|
||||
x = camera_number(camera_data.rotation.x),
|
||||
y = camera_number(camera_data.rotation.y, 0.0),
|
||||
z = camera_number(camera_data.rotation.z),
|
||||
}
|
||||
if not coords.x or not coords.y or not coords.z or not rotation.x or not rotation.z then
|
||||
Bridge.Debug("error", "[sky_phone] Rejected invalid housing camera data.")
|
||||
return
|
||||
end
|
||||
|
||||
local config = Config.Housing.Camera
|
||||
local ped = PlayerPedId()
|
||||
local camera = CreateCamWithParams(
|
||||
"DEFAULT_SCRIPTED_CAMERA",
|
||||
coords.x,
|
||||
coords.y,
|
||||
coords.z,
|
||||
rotation.x,
|
||||
rotation.y,
|
||||
rotation.z,
|
||||
config.FieldOfView,
|
||||
true,
|
||||
2
|
||||
)
|
||||
if not camera or camera == 0 then
|
||||
Bridge.Debug("error", "[sky_phone] Failed to create housing camera.")
|
||||
return
|
||||
end
|
||||
|
||||
camera_state = {
|
||||
camera = camera,
|
||||
frozen = IsEntityPositionFrozen(ped),
|
||||
night_vision = GetUsingnightvision(),
|
||||
ped = ped,
|
||||
provider = provider_name,
|
||||
}
|
||||
suspend_phone()
|
||||
FreezeEntityPosition(ped, true)
|
||||
SetFocusArea(coords.x, coords.y, coords.z, 0.0, 0.0, 0.0)
|
||||
SetCamActive(camera, true)
|
||||
RenderScriptCams(true, true, 350, true, true)
|
||||
|
||||
local maximum_left = camera_number(camera_data.maximumLeft)
|
||||
local maximum_right = camera_number(camera_data.maximumRight)
|
||||
local night_vision = camera_state.night_vision
|
||||
while camera_state and camera_state.camera == camera do
|
||||
Wait(0)
|
||||
DisableAllControlActions(0)
|
||||
if IsDisabledControlJustPressed(0, config.ExitControl)
|
||||
or IsPedDeadOrDying(ped, true)
|
||||
or GetResourceState(provider_name) ~= "started"
|
||||
then
|
||||
break
|
||||
end
|
||||
|
||||
local camera_rotation = GetCamRot(camera, 2)
|
||||
local next_x = camera_rotation.x
|
||||
local next_z = camera_rotation.z
|
||||
if IsDisabledControlPressed(0, config.LeftControl)
|
||||
and (not maximum_left or next_z < maximum_left)
|
||||
then
|
||||
next_z = next_z + config.RotateSpeed
|
||||
elseif IsDisabledControlPressed(0, config.RightControl)
|
||||
and (not maximum_right or next_z > maximum_right)
|
||||
then
|
||||
next_z = next_z - config.RotateSpeed
|
||||
end
|
||||
if IsDisabledControlPressed(0, config.UpControl) then
|
||||
next_x = math.min(85.0, next_x + config.RotateSpeed)
|
||||
elseif IsDisabledControlPressed(0, config.DownControl) then
|
||||
next_x = math.max(-85.0, next_x - config.RotateSpeed)
|
||||
end
|
||||
if next_x ~= camera_rotation.x or next_z ~= camera_rotation.z then
|
||||
SetCamRot(camera, next_x, camera_rotation.y, next_z, 2)
|
||||
end
|
||||
|
||||
local field_of_view = GetCamFov(camera)
|
||||
if IsDisabledControlJustPressed(0, config.ZoomInControl) then
|
||||
SetCamFov(camera, math.max(config.MinimumFieldOfView, field_of_view - 3.0))
|
||||
elseif IsDisabledControlJustPressed(0, config.ZoomOutControl) then
|
||||
SetCamFov(camera, math.min(config.MaximumFieldOfView, field_of_view + 3.0))
|
||||
end
|
||||
if IsDisabledControlJustPressed(0, config.NightVisionControl) then
|
||||
night_vision = not night_vision
|
||||
SetNightvision(night_vision)
|
||||
end
|
||||
end
|
||||
stop_camera()
|
||||
end
|
||||
|
||||
RegisterNUICallback("housing:overview", function(_, cb)
|
||||
local result = Bridge.Callbacks.Trigger("sky_phone:housing:overview", {})
|
||||
cb(result or { success = false, error = "request_failed" })
|
||||
end)
|
||||
|
||||
RegisterNUICallback("housing:key-candidates", function(data, cb)
|
||||
if type(data) ~= "table" or type(data.propertyId) ~= "string" then
|
||||
cb({ success = false, error = "invalid_request" })
|
||||
return
|
||||
end
|
||||
local result = server_prepare("key_candidates", data)
|
||||
cb(result or { success = false, error = "request_failed" })
|
||||
end)
|
||||
|
||||
RegisterNUICallback("housing:command", function(data, cb)
|
||||
if type(data) ~= "table" or type(data.action) ~= "string" or type(data.propertyId) ~= "string" then
|
||||
cb({ success = false, error = "invalid_request" })
|
||||
return
|
||||
end
|
||||
local actions = {
|
||||
grant_key = true,
|
||||
open_cctv = true,
|
||||
revoke_key = true,
|
||||
set_waypoint = true,
|
||||
toggle_lock = true,
|
||||
}
|
||||
if not actions[data.action] then
|
||||
cb({ success = false, error = "invalid_action" })
|
||||
return
|
||||
end
|
||||
|
||||
local prepared = server_prepare(data.action, data)
|
||||
if not prepared or not prepared.success or type(prepared.data) ~= "table" then
|
||||
cb(prepared or { success = false, error = "request_failed" })
|
||||
return
|
||||
end
|
||||
if data.action == "set_waypoint" then
|
||||
local coords = prepared.data.coords
|
||||
if type(coords) ~= "table" or not tonumber(coords.x) or not tonumber(coords.y) then
|
||||
cb({ success = false, error = "invalid_coordinates" })
|
||||
return
|
||||
end
|
||||
SetNewWaypoint(tonumber(coords.x) + 0.0, tonumber(coords.y) + 0.0)
|
||||
cb({ success = true })
|
||||
return
|
||||
end
|
||||
if data.action == "open_cctv" then
|
||||
if type(prepared.data.camera) ~= "table" or type(prepared.data.provider) ~= "string" then
|
||||
cb({ success = false, error = "cctv_unavailable" })
|
||||
return
|
||||
end
|
||||
cb({ success = true })
|
||||
CreateThread(function()
|
||||
run_camera(prepared.data.provider, prepared.data.camera)
|
||||
end)
|
||||
return
|
||||
end
|
||||
|
||||
local success, error_code = Bridge.Housing.Execute(prepared.data.provider, data.action, prepared.data)
|
||||
cb(success and { success = true } or { success = false, error = error_code or "provider_rejected" })
|
||||
end)
|
||||
|
||||
AddEventHandler("onResourceStop", function(resource_name)
|
||||
if resource_name == GetCurrentResourceName() and camera_state then
|
||||
stop_camera()
|
||||
end
|
||||
end)
|
||||
@@ -0,0 +1,48 @@
|
||||
Bridge.Callbacks.Register("sky_phone:housing:overview", function(source)
|
||||
if not SkyPhone.AllowOperation(
|
||||
source,
|
||||
"housing_overview",
|
||||
Config.Housing.OverviewRequestsPerMinute,
|
||||
60
|
||||
) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
end
|
||||
local session, error_response = SkyPhone.RequireSession(source)
|
||||
if not session then
|
||||
return error_response
|
||||
end
|
||||
local overview, error_code = Bridge.Housing.GetOverview(source)
|
||||
if not overview then
|
||||
return { success = false, error = error_code or "request_failed" }
|
||||
end
|
||||
return { success = true, data = overview }
|
||||
end)
|
||||
|
||||
Bridge.Callbacks.Register("sky_phone:housing:prepare", function(source, data)
|
||||
if not SkyPhone.AllowOperation(source, "housing_action", Config.Housing.ActionsPerMinute, 60) then
|
||||
return { success = false, error = "rate_limited" }
|
||||
end
|
||||
local session, error_response = SkyPhone.RequireSession(source)
|
||||
if not session then
|
||||
return error_response
|
||||
end
|
||||
if type(data) ~= "table" or type(data.action) ~= "string" then
|
||||
return { success = false, error = "invalid_request" }
|
||||
end
|
||||
local allowed_actions = {
|
||||
grant_key = true,
|
||||
key_candidates = true,
|
||||
open_cctv = true,
|
||||
revoke_key = true,
|
||||
set_waypoint = true,
|
||||
toggle_lock = true,
|
||||
}
|
||||
if not allowed_actions[data.action] then
|
||||
return { success = false, error = "invalid_action" }
|
||||
end
|
||||
local prepared, error_code = Bridge.Housing.Prepare(source, data.action, data)
|
||||
if not prepared then
|
||||
return { success = false, error = error_code or "action_failed" }
|
||||
end
|
||||
return { success = true, data = prepared }
|
||||
end)
|
||||
Reference in New Issue
Block a user