mirror of
https://github.com/sky-systems/sky_phone.git
synced 2026-08-29 02:01:40 +00:00
42ac955052
Add the Companies app, resilient call handling, custom app registry, vendor compatibility, storage policies, frontend bridge, tests, documentation, config, locale, and SQL migrations.
341 lines
13 KiB
Lua
341 lines
13 KiB
Lua
Bridge.Database.AfterMigration("sky_phone", function()
|
|
|
|
SkyPhoneSim = {}
|
|
|
|
local pending_insertions = {}
|
|
local operation_locks = {}
|
|
local sim_types = {
|
|
[Config.Sim.RegisteredItem] = "registered",
|
|
[Config.Sim.AnonymousItem] = "anonymous",
|
|
}
|
|
|
|
local function affected_rows(result)
|
|
if type(result) == "number" then
|
|
return result
|
|
end
|
|
return type(result) == "table" and tonumber(result.affectedRows) or 0
|
|
end
|
|
|
|
local function uuid()
|
|
local rows = Bridge.Database.Query("SELECT UUID() AS `id`", {})
|
|
if not rows[1] or type(rows[1].id) ~= "string" then
|
|
error("[sky_phone] Database did not generate a SIM UUID.")
|
|
end
|
|
return rows[1].id
|
|
end
|
|
|
|
local function reserve_sim(sim_type)
|
|
local sim_id
|
|
local number = SkyPhoneSimNumber.Reserve(uuid, function(candidate)
|
|
if SkyPhoneCompanies.IsServiceNumber(candidate) then
|
|
return false
|
|
end
|
|
sim_id = uuid()
|
|
local result = Bridge.Database.Query([[
|
|
INSERT IGNORE INTO `sky_phone_sims` (`id`, `phone_number`, `sim_type`)
|
|
VALUES (?, ?, ?)
|
|
]], { sim_id, candidate, sim_type })
|
|
return affected_rows(result) == 1
|
|
end, Config.Sim.NumberLength, Config.Sim.NumberPrefix)
|
|
if not number then
|
|
error("[sky_phone] Could not reserve a unique SIM number after 20 attempts.")
|
|
end
|
|
return { id = sim_id, phone_number = number, sim_type = sim_type }
|
|
end
|
|
|
|
local function load_sim(sim_id)
|
|
local rows = Bridge.Database.Query("SELECT * FROM `sky_phone_sims` WHERE `id` = ? LIMIT 1", { sim_id })
|
|
return rows[1]
|
|
end
|
|
|
|
local function sim_metadata(sim)
|
|
local metadata = {
|
|
sim_metadata_version = 1,
|
|
sim_id = sim.id,
|
|
phone_number = sim.phone_number,
|
|
formatted_number = SkyPhoneSimNumber.Format(sim.phone_number, Config.Sim.NumberGroups, Config.Sim.NumberLength, Config.Sim.NumberPrefix),
|
|
sim_type = sim.sim_type,
|
|
}
|
|
if sim.sim_type == "registered" and sim.owner_identifier then
|
|
metadata.firstname = sim.owner_firstname
|
|
metadata.lastname = sim.owner_lastname
|
|
metadata.birthdate = sim.owner_birthdate
|
|
metadata.registered_at = sim.registered_at
|
|
end
|
|
return metadata
|
|
end
|
|
|
|
local function resolve_used_sim(source, used_item, item_name)
|
|
local slot_id = used_item and (used_item.slot or used_item.id)
|
|
local slot = slot_id and Bridge.Inventory.GetSlot(source, slot_id) or nil
|
|
if slot and slot.name == item_name then
|
|
return slot
|
|
end
|
|
local slots = Bridge.Inventory.GetSlotsWithItem(source, item_name)
|
|
if #slots == 1 then
|
|
return slots[1]
|
|
end
|
|
Bridge.Debug("warn", "[sky_phone] Could not resolve exact SIM slot for source %s.", tostring(source))
|
|
return nil
|
|
end
|
|
|
|
local function ensure_sim(source, slot, sim_type)
|
|
if (tonumber(slot.amount or slot.count) or 0) ~= 1 then
|
|
return nil, "sim_stacked"
|
|
end
|
|
local metadata = slot.metadata or {}
|
|
local sim = metadata.sim_id and load_sim(metadata.sim_id) or nil
|
|
if metadata.sim_id and (not sim or sim.sim_type ~= sim_type or sim.phone_number ~= metadata.phone_number) then
|
|
return nil, "invalid_sim"
|
|
end
|
|
if sim and SkyPhoneCompanies.IsServiceNumber(sim.phone_number) then
|
|
Bridge.Debug(
|
|
"error",
|
|
"[sky_phone] SIM %s uses reserved company service number %s.",
|
|
tostring(sim.id),
|
|
tostring(sim.phone_number)
|
|
)
|
|
return nil, "invalid_sim"
|
|
end
|
|
if not sim then
|
|
sim = reserve_sim(sim_type)
|
|
if not Bridge.Inventory.SetSlotMetadata(source, slot.slot, sim_metadata(sim)) then
|
|
Bridge.Database.Query("DELETE FROM `sky_phone_sims` WHERE `id` = ?", { sim.id })
|
|
return nil, "metadata_unsupported"
|
|
end
|
|
end
|
|
return sim
|
|
end
|
|
|
|
local function list_phone_choices(source)
|
|
local choices = {}
|
|
for _, slot in ipairs(Bridge.Inventory.GetSlotsWithItem(source, Config.Phone.Item)) do
|
|
local imei = SkyPhone.EnsureDevice(source, slot)
|
|
if imei then
|
|
local device = SkyPhone.LoadDevice(imei)
|
|
choices[#choices + 1] = {
|
|
imei = imei,
|
|
name = device.device_name,
|
|
occupied = device.sim_id ~= nil,
|
|
number = device.phone_number,
|
|
}
|
|
end
|
|
end
|
|
return choices
|
|
end
|
|
|
|
local function rollback_phone_metadata(source, phone_slot, old_sim)
|
|
local metadata = phone_slot.metadata or {}
|
|
metadata.sim_id = old_sim and old_sim.id or nil
|
|
metadata.phone_number = old_sim and old_sim.phone_number or nil
|
|
metadata.formatted_number = old_sim and SkyPhoneSimNumber.Format(old_sim.phone_number, Config.Sim.NumberGroups, Config.Sim.NumberLength, Config.Sim.NumberPrefix) or nil
|
|
Bridge.Inventory.SetSlotMetadata(source, phone_slot.slot, metadata)
|
|
end
|
|
|
|
local function insert_sim(source, phone_imei, confirmed)
|
|
if operation_locks[source] then
|
|
return { success = false, error = "operation_in_progress" }
|
|
end
|
|
local pending = pending_insertions[source]
|
|
if not pending or GetGameTimer() - pending.created_at > 60000 then
|
|
pending_insertions[source] = nil
|
|
return { success = false, error = "sim_request_expired" }
|
|
end
|
|
if SkyPhoneCompanies.IsServiceNumber(pending.sim.phone_number) then
|
|
pending_insertions[source] = nil
|
|
Bridge.Debug(
|
|
"error",
|
|
"[sky_phone] Refused to insert SIM %s with reserved company service number %s.",
|
|
tostring(pending.sim.id),
|
|
tostring(pending.sim.phone_number)
|
|
)
|
|
return { success = false, error = "invalid_sim" }
|
|
end
|
|
local sim_slot = Bridge.Inventory.GetSlot(source, pending.slot)
|
|
if not sim_slot or sim_slot.name ~= pending.item_name or not sim_slot.metadata
|
|
or sim_slot.metadata.sim_id ~= pending.sim.id then
|
|
return { success = false, error = "sim_not_owned" }
|
|
end
|
|
local phone_matches = SkyPhone.FindDeviceSlots(source, phone_imei)
|
|
if not phone_matches[1] then
|
|
return { success = false, error = "phone_not_owned" }
|
|
end
|
|
local phone_slot = phone_matches[1]
|
|
local device = SkyPhone.LoadDevice(phone_imei)
|
|
local old_sim = device.sim_id and load_sim(device.sim_id) or nil
|
|
if old_sim and not confirmed then
|
|
return { success = false, error = "confirmation_required", data = { requiresConfirmation = true } }
|
|
end
|
|
|
|
operation_locks[source] = true
|
|
local phone_metadata = phone_slot.metadata or {}
|
|
phone_metadata.sim_id = pending.sim.id
|
|
phone_metadata.phone_number = pending.sim.phone_number
|
|
phone_metadata.formatted_number = SkyPhoneSimNumber.Format(pending.sim.phone_number, Config.Sim.NumberGroups, Config.Sim.NumberLength, Config.Sim.NumberPrefix)
|
|
if not Bridge.Inventory.SetSlotMetadata(source, phone_slot.slot, phone_metadata) then
|
|
operation_locks[source] = nil
|
|
return { success = false, error = "metadata_unsupported" }
|
|
end
|
|
if Bridge.Inventory.RemoveItem(source, pending.item_name, 1, pending.slot) ~= 1 then
|
|
rollback_phone_metadata(source, phone_slot, old_sim)
|
|
operation_locks[source] = nil
|
|
return { success = false, error = "sim_not_owned" }
|
|
end
|
|
local old_item_name = old_sim and (old_sim.sim_type == "registered" and Config.Sim.RegisteredItem or Config.Sim.AnonymousItem) or nil
|
|
if old_sim and not Bridge.Inventory.AddItem(source, old_item_name, 1, pending.slot, sim_metadata(old_sim)) then
|
|
Bridge.Inventory.AddItem(source, pending.item_name, 1, pending.slot, sim_metadata(pending.sim))
|
|
rollback_phone_metadata(source, phone_slot, old_sim)
|
|
operation_locks[source] = nil
|
|
return { success = false, error = "inventory_full" }
|
|
end
|
|
|
|
local transaction = {
|
|
{
|
|
query = "UPDATE `sky_phone_devices` SET `sim_id` = ? WHERE `imei` = ?",
|
|
params = { pending.sim.id, phone_imei },
|
|
},
|
|
}
|
|
if pending.sim.sim_type == "registered" and not pending.sim.owner_identifier then
|
|
transaction[#transaction + 1] = {
|
|
query = [[
|
|
UPDATE `sky_phone_sims`
|
|
SET `owner_identifier` = ?, `owner_firstname` = ?, `owner_lastname` = ?,
|
|
`owner_birthdate` = ?, `registered_at` = CURRENT_TIMESTAMP
|
|
WHERE `id` = ? AND `owner_identifier` IS NULL
|
|
]],
|
|
params = {
|
|
Bridge.Framework.GetIdentifier(source),
|
|
Bridge.Framework.GetFirstname(source),
|
|
Bridge.Framework.GetLastname(source),
|
|
Bridge.Framework.GetBirthdate(source),
|
|
pending.sim.id,
|
|
},
|
|
}
|
|
end
|
|
if not Bridge.Database.Transaction(transaction) then
|
|
if old_sim then
|
|
Bridge.Inventory.RemoveItem(source, old_sim.sim_type == "registered" and Config.Sim.RegisteredItem or Config.Sim.AnonymousItem, 1, pending.slot)
|
|
end
|
|
Bridge.Inventory.AddItem(source, pending.item_name, 1, pending.slot, sim_metadata(pending.sim))
|
|
rollback_phone_metadata(source, phone_slot, old_sim)
|
|
operation_locks[source] = nil
|
|
return { success = false, error = "request_failed" }
|
|
end
|
|
|
|
pending_insertions[source] = nil
|
|
operation_locks[source] = nil
|
|
if old_sim then
|
|
SkyPhoneCompanies.ClearCallAvailability(source)
|
|
SkyPhoneCalls.EndForSim(old_sim.id, "sim_removed")
|
|
end
|
|
SkyPhone.RefreshDevice(phone_imei)
|
|
TriggerClientEvent("sky_phone:sim:picker-close", source)
|
|
return { success = true }
|
|
end
|
|
|
|
local function use_sim(source, used_item)
|
|
local item_name = used_item and used_item.name
|
|
local sim_type = item_name and sim_types[item_name]
|
|
if not sim_type then
|
|
Bridge.Debug("warn", "[sky_phone] Usable SIM callback received an invalid item for source %s.", tostring(source))
|
|
return false
|
|
end
|
|
if operation_locks[source] then
|
|
TriggerClientEvent("sky_phone:device:error", source, "operation_in_progress")
|
|
return false
|
|
end
|
|
operation_locks[source] = true
|
|
local slot = resolve_used_sim(source, used_item, item_name)
|
|
if not slot then
|
|
operation_locks[source] = nil
|
|
TriggerClientEvent("sky_phone:device:error", source, "sim_slot_missing")
|
|
return false
|
|
end
|
|
local sim, error_code = ensure_sim(source, slot, sim_type)
|
|
if not sim then
|
|
operation_locks[source] = nil
|
|
TriggerClientEvent("sky_phone:device:error", source, error_code)
|
|
return false
|
|
end
|
|
local choices = list_phone_choices(source)
|
|
if #choices == 0 then
|
|
operation_locks[source] = nil
|
|
TriggerClientEvent("sky_phone:device:error", source, "phone_required")
|
|
return false
|
|
end
|
|
pending_insertions[source] = {
|
|
created_at = GetGameTimer(),
|
|
item_name = item_name,
|
|
sim = sim,
|
|
slot = slot.slot,
|
|
}
|
|
operation_locks[source] = nil
|
|
if #choices == 1 and not choices[1].occupied then
|
|
return insert_sim(source, choices[1].imei, false).success
|
|
end
|
|
TriggerClientEvent("sky_phone:sim:picker", source, {
|
|
choices = choices,
|
|
number = sim.phone_number,
|
|
})
|
|
return true
|
|
end
|
|
|
|
Bridge.Inventory.RegisterUsableItem(Config.Sim.RegisteredItem, use_sim)
|
|
Bridge.Inventory.RegisterUsableItem(Config.Sim.AnonymousItem, use_sim)
|
|
|
|
Bridge.Callbacks.Register("sky_phone:sim:insert", function(source, data)
|
|
if type(data) ~= "table" or not SkyPhoneImei.IsValid(data.imei) then
|
|
return { success = false, error = "invalid_request" }
|
|
end
|
|
return insert_sim(source, data.imei, data.confirmed == true)
|
|
end)
|
|
|
|
Bridge.Callbacks.Register("sky_phone:sim:eject", function(source)
|
|
if operation_locks[source] then
|
|
return { success = false, error = "operation_in_progress" }
|
|
end
|
|
local session, error_response = SkyPhone.RequireSession(source)
|
|
if not session then
|
|
return error_response
|
|
end
|
|
local device = SkyPhone.LoadDevice(session.imei)
|
|
local sim = device and device.sim_id and load_sim(device.sim_id) or nil
|
|
if not sim then
|
|
return { success = false, error = "no_sim" }
|
|
end
|
|
local phone_slot = Bridge.Inventory.GetSlot(source, session.slot)
|
|
local item_name = sim.sim_type == "registered" and Config.Sim.RegisteredItem or Config.Sim.AnonymousItem
|
|
local metadata = sim_metadata(sim)
|
|
if not Bridge.Inventory.CanCarryItem(source, item_name, 1, metadata) then
|
|
return { success = false, error = "inventory_full" }
|
|
end
|
|
|
|
operation_locks[source] = true
|
|
rollback_phone_metadata(source, phone_slot, nil)
|
|
if not Bridge.Inventory.AddItem(source, item_name, 1, nil, metadata) then
|
|
rollback_phone_metadata(source, phone_slot, sim)
|
|
operation_locks[source] = nil
|
|
return { success = false, error = "inventory_full" }
|
|
end
|
|
if affected_rows(Bridge.Database.Query("UPDATE `sky_phone_devices` SET `sim_id` = NULL WHERE `imei` = ? AND `sim_id` = ?", {
|
|
session.imei,
|
|
sim.id,
|
|
})) ~= 1 then
|
|
Bridge.Inventory.RemoveItem(source, item_name, 1, nil, metadata)
|
|
rollback_phone_metadata(source, phone_slot, sim)
|
|
operation_locks[source] = nil
|
|
return { success = false, error = "request_failed" }
|
|
end
|
|
operation_locks[source] = nil
|
|
SkyPhoneCompanies.ClearCallAvailability(source)
|
|
SkyPhoneCalls.EndForSim(sim.id, "sim_removed")
|
|
SkyPhone.RefreshDevice(session.imei)
|
|
return { success = true }
|
|
end)
|
|
|
|
AddEventHandler("playerDropped", function()
|
|
pending_insertions[source] = nil
|
|
operation_locks[source] = nil
|
|
end)
|
|
end)
|